/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // The header under test. #include "mozilla/StackWalk.h" #include "mozilla/Assertions.h" #include "mozilla/Attributes.h" #include #include "gtest/gtest.h" MOZ_EXPORT bool gStackWalkTesterDummy = true; struct StackWalkTester; // Descriptor of the recursive function calls wanted, and for each of them // whether to perform tail call optimization or not. struct CallInfo { int (*mFunc)(int aDepth, int aLastSkipped, int aIgnored, StackWalkTester& aTester); bool mTailCall; bool TailCall() { #if defined(__i386__) || defined(MOZ_CODE_COVERAGE) // We can't make tail calls happen on i386 because all arguments to // functions are on the stack, so the stack pointer needs to be updated // before the call and restored after the call, so tail call optimization // never happens. // Similarly, code-coverage flags don't guarantee that tail call // optimization will happen. return false; #else return mTailCall; #endif } }; struct PCRange { void* mStart; void* mEnd; }; // PCRange pretty printer for gtest assertions. std::ostream& operator<<(std::ostream& aStream, const PCRange& aRange) { aStream << aRange.mStart; aStream << "-"; aStream << aRange.mEnd; return aStream; } // Allow to use EXPECT_EQ with a vector of PCRanges and a vector of plain // addresses, allowing a more useful output when the test fails, showing // both lists. bool operator==(const std::vector& aRanges, const std::vector& aPtrs) { if (aRanges.size() != aPtrs.size()) { return false; } for (size_t i = 0; i < aRanges.size(); i++) { auto range = aRanges[i]; auto ptr = reinterpret_cast(aPtrs[i]); if (ptr <= reinterpret_cast(range.mStart) || ptr >= reinterpret_cast(range.mEnd)) { return false; } } return true; } struct StackWalkTester { // Description of the recursion of functions to perform for the testcase. std::vector mFuncCalls; // Collection of PCs reported by MozStackWalk. std::vector mFramePCs; // Collection of PCs expected per what was observed while recursing. std::vector mExpectedFramePCs; // The aFirstFramePC value that will be passed to MozStackWalk. void* mFirstFramePC = nullptr; // Callback to be given to the stack walker. // aClosure should point at an instance of this class. static void StackWalkCallback(uint32_t aFrameNumber, void* aPC, void* aSP, void* aClosure) { ASSERT_NE(aClosure, nullptr); StackWalkTester& tester = *reinterpret_cast(aClosure); tester.mFramePCs.push_back(aPC); EXPECT_EQ(tester.mFramePCs.size(), size_t(aFrameNumber)) << "Frame number doesn't match"; } // Callers of this function get a range of addresses with: // ``` // label: // recursion(); // AddExpectedPC(&&label); // ``` // This intends to record the range from label to the return of AddExpectedPC. // The ideal code would be: // ``` // recursion(); // label: // AddExpectedPC(&&label); // ``` // and we wouldn't need to keep ranges. But while this works fine with Clang, // GCC actually sometimes reorders code such the address received by // AddExpectedPC is the address *before* the recursion. // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=99784 Using a label before the // recursion and CallerPC() from a function call after the recursion makes it // less likely for things to go wrong. MOZ_NEVER_INLINE void AddExpectedPC(void* aPC) { mExpectedFramePCs.push_back({aPC, CallerPC()}); } // Function intended to be called in sequence for recursion. // CallInfo lists are meant to contain a sequence of IntermediateCallback<1>, // IntermediateCallback<2>, etc. // aDepth is a counter of how deep the recursion has gone so far; // aLastSkipped is the depth of the last frame we want skipped in the // testcase; aIgnored is there to avoid the compiler merging both recursive // function calls, which would prevent tail call optimization happening on one // of them. aTester is the instance of this class for the testcase. template MOZ_NEVER_INLINE MOZ_EXPORT static int IntermediateCallback( int aDepth, int aLastSkipped, int aIgnored, StackWalkTester& aTester) { auto& callInfo = aTester.mFuncCalls.at(aDepth + 1); if (aDepth == aLastSkipped) { aTester.mFirstFramePC = CallerPC(); } if (aTester.mFuncCalls.at(aDepth).TailCall()) { return callInfo.mFunc(aDepth + 1, aLastSkipped, Id, aTester); // Since we're doing a tail call, we're not expecting this frame appearing // in the trace. } here: callInfo.mFunc(aDepth + 1, aLastSkipped, Id + 1, aTester); aTester.AddExpectedPC(&&here); return 0; } #if defined(__clang__) __attribute__((no_sanitize("function"))) #endif MOZ_NEVER_INLINE MOZ_EXPORT static void LeafCallback(int aDepth, int aLastSkipped, int aIgnored, StackWalkTester& aTester) { if (aDepth == aLastSkipped) { aTester.mFirstFramePC = CallerPC(); } if (aTester.mFuncCalls.at(aDepth).TailCall()) { // For the same reason that we have the aIgnored argument on these // callbacks, we need to avoid both MozStackWalk calls to be merged by the // compiler, so we use different values of aMaxFrames for that. return MozStackWalk(StackWalkTester::StackWalkCallback, aTester.mFirstFramePC, /*aMaxFrames*/ 19, &aTester); // Since we're doing a tail call, we're not expecting this frame appearing // in the trace. } here: MozStackWalk(StackWalkTester::StackWalkCallback, aTester.mFirstFramePC, /*aMaxFrames*/ 20, &aTester); aTester.AddExpectedPC(&&here); // Because we return nothing from this function, simply returning here would // produce a tail-call optimization, which we explicitly don't want to // happen. So we add a branch that depends on an extern value to prevent // that from happening. MOZ_RELEASE_ASSERT(gStackWalkTesterDummy); } explicit StackWalkTester(std::initializer_list aFuncCalls) : mFuncCalls(aFuncCalls) {} // Dump a vector of PCRanges as WalkTheStack would, for test failure output. // Only the end of the range is shown. Not ideal, but // MozFormatCodeAddressDetails only knows to deal with one address at a time. // The full ranges would be printed by EXPECT_EQ anyways. static std::string DumpFrames(std::vector& aFramePCRanges) { std::vector framePCs; framePCs.reserve(aFramePCRanges.size()); for (auto range : aFramePCRanges) { framePCs.push_back(range.mEnd); } return DumpFrames(framePCs); } // Dump a vector of addresses as WalkTheStack would, for test failure output. static std::string DumpFrames(std::vector& aFramePCs) { size_t n = 0; std::string result; for (auto* framePC : aFramePCs) { char buf[1024]; MozCodeAddressDetails details; result.append(" "); n++; if (MozDescribeCodeAddress(framePC, &details)) { int length = MozFormatCodeAddressDetails(buf, sizeof(buf), n, framePC, &details); result.append(buf, std::min(length, (int)sizeof(buf) - 1)); } else { result.append("MozDescribeCodeAddress failed"); } result.append("\n"); } return result; } // Dump a description of the given test case. static std::string DumpFuncCalls(std::vector& aFuncCalls) { std::string result; for (auto funcCall : aFuncCalls) { MozCodeAddressDetails details; result.append(" "); if (MozDescribeCodeAddress(reinterpret_cast(funcCall.mFunc), &details)) { result.append(details.function); if (funcCall.TailCall()) { result.append(" tail call"); } } else { result.append("MozDescribeCodeAddress failed"); } result.append("\n"); } return result; } MOZ_EXPORT MOZ_NEVER_INLINE void RunTest(int aLastSkipped) { ASSERT_TRUE(aLastSkipped < (int)mFuncCalls.size()); mFramePCs.clear(); mExpectedFramePCs.clear(); mFirstFramePC = nullptr; auto& callInfo = mFuncCalls.at(0); here: callInfo.mFunc(0, aLastSkipped, 0, *this); AddExpectedPC(&&here); if (aLastSkipped < 0) { aLastSkipped = mFuncCalls.size(); } for (int i = (int)mFuncCalls.size() - 1; i >= aLastSkipped; i--) { if (!mFuncCalls.at(i).TailCall()) { mExpectedFramePCs.erase(mExpectedFramePCs.begin()); } } mFramePCs.resize(std::min(mExpectedFramePCs.size(), mFramePCs.size())); EXPECT_EQ(mExpectedFramePCs, mFramePCs) << "Expected frames:\n" << DumpFrames(mExpectedFramePCs) << "Found frames:\n" << DumpFrames(mFramePCs) << "Function calls data (last skipped: " << aLastSkipped << "):\n" << DumpFuncCalls(mFuncCalls); } }; TEST(TestStackWalk, StackWalk) { const auto foo = StackWalkTester::IntermediateCallback<1>; const auto bar = StackWalkTester::IntermediateCallback<2>; const auto qux = StackWalkTester::IntermediateCallback<3>; const auto leaf = reinterpret_cast( StackWalkTester::LeafCallback); const std::initializer_list tests[] = { {{foo, false}, {bar, false}, {qux, false}, {leaf, false}}, {{foo, false}, {bar, true}, {qux, false}, {leaf, false}}, {{foo, false}, {bar, false}, {qux, false}, {leaf, true}}, {{foo, true}, {bar, false}, {qux, true}, {leaf, true}}, }; for (auto test : tests) { StackWalkTester tester(test); for (int i = -1; i < (int)test.size(); i++) { tester.RunTest(i); } } }