Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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.
// 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);
}
}
}