Skip to content

Commit

Permalink
Fix ExtractGolangInterface to handle go 1.20 binaries and later (#2093
Browse files Browse the repository at this point in the history
)

Summary: Fix `ExtractGolangInterface` to handle go 1.20 binaries and
later

This is part of the work to deprecate or fix dynamic logging

Relevant Issues: #666

Type of change: /kind bugfix

Test Plan: New tests pass

---------

Signed-off-by: Dom Del Nano <[email protected]>
  • Loading branch information
ddelnano authored Feb 3, 2025
1 parent aa097f2 commit 9dfdcf7
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 33 deletions.
1 change: 1 addition & 0 deletions src/stirling/obj_tools/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ pl_cc_test(
"//src/stirling/obj_tools/testdata/go:test_binaries",
"//src/stirling/obj_tools/testdata/go:test_go_1_17_binary",
"//src/stirling/obj_tools/testdata/go:test_go_1_19_binary",
"//src/stirling/obj_tools/testdata/go:test_go_1_21_binary",
],
deps = [
":cc_library",
Expand Down
26 changes: 19 additions & 7 deletions src/stirling/obj_tools/go_syms.cc
Original file line number Diff line number Diff line change
Expand Up @@ -144,16 +144,28 @@ StatusOr<std::string> ReadGoBuildVersion(ElfReader* elf_reader) {
return ReadGoString(elf_reader, ptr_size, ptr_addr, read_ptr);
}

// Prefixes used to search for itable symbols in the binary. Follows the format:
// <prefix>.<type_name>,<interface_name>. i.e. go.itab.<type_name>,<interface_name>
constexpr std::array<std::string_view, 2> kITablePrefixes = {
"go:itab.", // Prefix used by Go 1.20 binaries and later.
"go.itab.", // Prefix used by Go 1.19 binaries and earlier.
};

StatusOr<absl::flat_hash_map<std::string, std::vector<IntfImplTypeInfo>>> ExtractGolangInterfaces(
ElfReader* elf_reader) {
absl::flat_hash_map<std::string, std::vector<IntfImplTypeInfo>> interface_types;

// All itable objects in the symbols are prefixed with this string.
const std::string_view kITablePrefix("go.itab.");

PX_ASSIGN_OR_RETURN(std::vector<ElfReader::SymbolInfo> itable_symbols,
elf_reader->SearchSymbols(kITablePrefix, SymbolMatchType::kPrefix,
/*symbol_type*/ ELFIO::STT_OBJECT));
std::vector<ElfReader::SymbolInfo> itable_symbols;
std::string_view iTablePrefix;
for (const auto& prefix : kITablePrefixes) {
PX_ASSIGN_OR_RETURN(itable_symbols,
elf_reader->SearchSymbols(prefix, SymbolMatchType::kPrefix,
/*symbol_type*/ ELFIO::STT_OBJECT));
if (!itable_symbols.empty()) {
iTablePrefix = prefix;
break;
}
}

for (const auto& sym : itable_symbols) {
// Expected format is:
Expand All @@ -166,7 +178,7 @@ StatusOr<absl::flat_hash_map<std::string, std::vector<IntfImplTypeInfo>>> Extrac

std::string_view interface_name = sym_split[1];
std::string_view type = sym_split[0];
type.remove_prefix(kITablePrefix.size());
type.remove_prefix(iTablePrefix.size());

IntfImplTypeInfo info;

Expand Down
109 changes: 83 additions & 26 deletions src/stirling/obj_tools/go_syms_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
#include "src/stirling/obj_tools/go_syms.h"

#include <memory>
#include <tuple>
#include <utility>

#include "src/common/testing/testing.h"

Expand All @@ -27,7 +29,9 @@ namespace stirling {
namespace obj_tools {

using ::testing::Field;
using ::testing::Matcher;
using ::testing::StrEq;
using ::testing::UnorderedElementsAre;

constexpr std::string_view kTestGoLittleEndiani386BinaryPath =
"src/stirling/obj_tools/testdata/go/test_go1_13_i386_binary";
Expand All @@ -37,6 +41,8 @@ constexpr std::string_view kTestGoLittleEndianBinaryPath =

constexpr std::string_view kTestGoBinaryPath =
"src/stirling/obj_tools/testdata/go/test_go_1_19_binary";
constexpr std::string_view kTestGo1_21BinaryPath =
"src/stirling/obj_tools/testdata/go/test_go_1_21_binary";

// The "endian agnostic" case refers to where the Go version data is varint encoded
// directly within the buildinfo header. See the following reference for more details.
Expand Down Expand Up @@ -68,44 +74,95 @@ TEST(IsGoExecutableTest, WorkingOnBasicGoBinary) {
EXPECT_TRUE(IsGoExecutable(elf_reader.get()));
}

TEST(ElfGolangItableTest, ExtractInterfaceTypes) {
const std::string kPath = px::testing::BazelRunfilePath(kTestGoBinaryPath);
class ElfGolangItableTest
: public ::testing::TestWithParam<std::tuple<
std::string,
Matcher<const std::vector<std::pair<std::string, std::vector<IntfImplTypeInfo>>>>>> {};

INSTANTIATE_TEST_SUITE_P(
ElfGolangItableTestSuite, ElfGolangItableTest,
::testing::Values(
std::make_tuple(
kTestGo1_21BinaryPath,
UnorderedElementsAre(
Pair("fmt.State",
UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name, "*fmt.pp"))),
Pair("internal/bisect.Writer",
UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name,
"*internal/godebug.runtimeStderr"))),
Pair("internal/reflectlite.Type",
UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name,
"internal/reflectlite.rtype"))),
Pair("error",
UnorderedElementsAre(
Field(&IntfImplTypeInfo::type_name, "main.IntStruct"),
Field(&IntfImplTypeInfo::type_name, "*errors.errorString"),
Field(&IntfImplTypeInfo::type_name, "syscall.Errno"),
Field(&IntfImplTypeInfo::type_name, "*io/fs.PathError"),
Field(&IntfImplTypeInfo::type_name, "runtime.errorString"),
Field(&IntfImplTypeInfo::type_name, "internal/poll.errNetClosing"),
Field(&IntfImplTypeInfo::type_name,
"*internal/poll.DeadlineExceededError"),
Field(&IntfImplTypeInfo::type_name, "*internal/bisect.parseError"))),
Pair("io.Writer",
UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name, "*os.File"))),
Pair("sort.Interface", UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name,
"*internal/fmtsort.SortedMap"))),
Pair("reflect.Type",
UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name, "*reflect.rtype"))),
Pair("math/rand.Source64", UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name,
"*math/rand.fastSource"))),
Pair("math/rand.Source", UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name,
"*math/rand.lockedSource"),
Field(&IntfImplTypeInfo::type_name,
"*math/rand.fastSource"))))),
std::make_tuple(
kTestGoBinaryPath,
UnorderedElementsAre(
Pair("error",
UnorderedElementsAre(
Field(&IntfImplTypeInfo::type_name, "main.IntStruct"),
Field(&IntfImplTypeInfo::type_name, "*errors.errorString"),
Field(&IntfImplTypeInfo::type_name, "*io/fs.PathError"),
Field(&IntfImplTypeInfo::type_name,
"*internal/poll.DeadlineExceededError"),
Field(&IntfImplTypeInfo::type_name, "internal/poll.errNetClosing"),
Field(&IntfImplTypeInfo::type_name, "runtime.errorString"),
Field(&IntfImplTypeInfo::type_name, "syscall.Errno"))),
Pair("sort.Interface", UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name,
"*internal/fmtsort.SortedMap"))),
Pair("math/rand.Source", UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name,
"*math/rand.lockedSource"))),
Pair("io.Writer",
UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name, "*os.File"))),
Pair("internal/reflectlite.Type",
UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name,
"*internal/reflectlite.rtype"))),
Pair("reflect.Type",
UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name, "*reflect.rtype"))),
Pair("fmt.State",
UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name, "*fmt.pp")))))));

TEST_P(ElfGolangItableTest, ExtractInterfaceTypes) {
const std::string kPath = px::testing::BazelRunfilePath(std::get<0>(GetParam()));

ASSERT_OK_AND_ASSIGN(std::unique_ptr<ElfReader> elf_reader, ElfReader::Create(kPath));
ASSERT_OK_AND_ASSIGN(const auto interfaces, ExtractGolangInterfaces(elf_reader.get()));

ASSERT_OK_AND_ASSIGN(const auto interfaces_map, ExtractGolangInterfaces(elf_reader.get()));
std::vector<std::pair<std::string, std::vector<IntfImplTypeInfo>>> interfaces(
interfaces_map.begin(), interfaces_map.end());

// Check for `bazel coverage` so we can bypass the final checks.
// Note that we still get accurate coverage metrics, because this only skips the final check.
// Ideally, we'd get bazel to deterministically build test_go_binary,
// but it's not easy to tell bazel to use a different config for just one target.

#ifdef PL_COVERAGE
LOG(INFO) << "Whoa...`bazel coverage` is messaging with test_go_binary. Shame on you bazel. "
"Ending this test early.";
return;
#else
EXPECT_THAT(
interfaces,
UnorderedElementsAre(
Pair("error",
UnorderedElementsAre(
Field(&IntfImplTypeInfo::type_name, "main.IntStruct"),
Field(&IntfImplTypeInfo::type_name, "*errors.errorString"),
Field(&IntfImplTypeInfo::type_name, "*io/fs.PathError"),
Field(&IntfImplTypeInfo::type_name, "*internal/poll.DeadlineExceededError"),
Field(&IntfImplTypeInfo::type_name, "internal/poll.errNetClosing"),
Field(&IntfImplTypeInfo::type_name, "runtime.errorString"),
Field(&IntfImplTypeInfo::type_name, "syscall.Errno"))),
Pair("sort.Interface", UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name,
"*internal/fmtsort.SortedMap"))),
Pair("math/rand.Source", UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name,
"*math/rand.lockedSource"))),
Pair("io.Writer", UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name, "*os.File"))),
Pair("internal/reflectlite.Type",
UnorderedElementsAre(
Field(&IntfImplTypeInfo::type_name, "*internal/reflectlite.rtype"))),
Pair("reflect.Type",
UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name, "*reflect.rtype"))),
Pair("fmt.State", UnorderedElementsAre(Field(&IntfImplTypeInfo::type_name, "*fmt.pp")))));
EXPECT_THAT(interfaces, std::get<1>(GetParam()));
#endif
}

Expand Down

0 comments on commit 9dfdcf7

Please sign in to comment.