280 lines
10 KiB
C++
280 lines
10 KiB
C++
/* -*- 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 <algorithm>
|
|
|
|
#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<PCRange>& aRanges,
|
|
const std::vector<void*>& 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<uintptr_t>(aPtrs[i]);
|
|
if (ptr <= reinterpret_cast<uintptr_t>(range.mStart) ||
|
|
ptr >= reinterpret_cast<uintptr_t>(range.mEnd)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
struct StackWalkTester {
|
|
// Description of the recursion of functions to perform for the testcase.
|
|
std::vector<CallInfo> mFuncCalls;
|
|
// Collection of PCs reported by MozStackWalk.
|
|
std::vector<void*> mFramePCs;
|
|
// Collection of PCs expected per what was observed while recursing.
|
|
std::vector<PCRange> 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<StackWalkTester*>(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 <int Id>
|
|
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<CallInfo> 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<PCRange>& aFramePCRanges) {
|
|
std::vector<void*> 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<void*>& 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<CallInfo>& aFuncCalls) {
|
|
std::string result;
|
|
for (auto funcCall : aFuncCalls) {
|
|
MozCodeAddressDetails details;
|
|
result.append(" ");
|
|
if (MozDescribeCodeAddress(reinterpret_cast<void*>(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<int (*)(int, int, int, StackWalkTester&)>(
|
|
StackWalkTester::LeafCallback);
|
|
|
|
const std::initializer_list<CallInfo> 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);
|
|
}
|
|
}
|
|
}
|