如何查找全局静态初始化

6
我刚读了这篇很棒的文章:http://neugierig.org/software/chromium/notes/2011/08/static-initializers.html,接着我尝试了:https://gcc.gnu.org/onlinedocs/gccint/Initialization.html 然而,对于寻找初始值的内容对我不起作用。 .ctors 部分不可用,但我可以找到 .init_array(请参见Can't find .dtors and .ctors in binary)。但我如何解释这个输出呢?我的意思是,将页面大小总和相加也可以通过 size 命令及其 .bss 列来处理——或者我错过了什么吗?
此外,nm 不报告任何 *_GLOBAL__I_* 符号,只有 *_GLOBAL__N_* 函数和更有趣的 _GLOBAL__sub_I_somefile.cpp 条目。后者可能表示具有全局初始化的文件。但我能否以某种方式获得正在运行的构造函数列表?理想情况下,工具会给我一个列表。
Foo::Foo in file1.cpp:12
Bar::Bar in file2.cpp:45
...

(假设我有调试符号可用)。是否有这样的工具?如果没有,怎么编写呢?.init_array部分是否包含指向可以通过一些DWARF魔法转换为上述内容的代码的指针?

1
我已经发现 nm -SlC --size-sort <ELF> | grep -F ' b ' 非常有帮助。它可以找到许多全局的 stringmap 和其他类似的东西。 - Trass3r
2个回答

3

正如您已经观察到的那样,构造函数/初始化函数的实现细节高度依赖于编译器(版本)。虽然我不知道是否有工具可以做到这一点,但当前的GCC/clang版本所做的事情足够简单,可以让一个小脚本完成: .init_array只是一个入口点列表。objdump -s可用于加载列表,nm可用于查找符号名称。以下是一个Python脚本,可以为任何由上述编译器生成的二进制文件工作:

#!/usr/bin/env python
import os
import sys

# Load .init_array section
objdump_output = os.popen("objdump -s '%s' -j .init_array" % (sys.argv[1].replace("'", r"\'"),)).read()
is_64bit = "x86-64" in objdump_output
init_array = objdump_output[objdump_output.find("Contents of section .init_array:") + 33:]
initializers = []
for line in init_array.split("\n"):
    parts = line.split()
    if not parts:
        continue
    parts.pop(0)  # Remove offset
    parts.pop(-1) # Remove ascii representation

    if is_64bit:
        # 64bit pointers are 8 bytes long
        parts = [ "".join(parts[i:i+2]) for i in range(0, len(parts), 2) ]

    # Fix endianess
    parts = [ "".join(reversed([ x[i:i+2] for i in range(0, len(x), 2) ])) for x in parts ]

    initializers += parts

# Load disassembly for c++ constructors
dis_output = os.popen("objdump -d '%s' | c++filt" % (sys.argv[1].replace("'", r"\'"), )).read()
def find_associated_constructor(disassembly, symbol):
    # Find associated __static_initialization function
    loc = disassembly.find("<%s>" % symbol)
    if loc < 0:
        return False
    loc = disassembly.find(" <", loc)
    if loc < 0:
        return False
    symbol = disassembly[loc+2:disassembly.find("\n", loc)][:-1]
    if symbol[:23] != "__static_initialization":
        return False
    address = disassembly[disassembly.rfind(" ", 0, loc)+1:loc]
    loc = disassembly.find("%s <%s>" % (address, symbol))
    if loc < 0:
        return False
    # Find all callq's in that function
    end_of_function = disassembly.find("\n\n", loc)
    symbols = []
    while loc < end_of_function:
        loc = disassembly.find("callq", loc)
        if loc < 0 or loc > end_of_function:
            break
        loc = disassembly.find("<", loc)
        symbols.append(disassembly[loc+1:disassembly.find("\n", loc)][:-1])
    return symbols

# Load symbol names, if available
nm_output = os.popen("nm '%s'" % (sys.argv[1].replace("'", r"\'"), )).read()
nm_symbols = {}
for line in nm_output.split("\n"):
    parts = line.split()
    if not parts:
        continue
    nm_symbols[parts[0]] = parts[-1]

# Output a list of initializers
print("Initializers:")
for initializer in initializers:
    symbol = nm_symbols[initializer] if initializer in nm_symbols else "???"
    constructor = find_associated_constructor(dis_output, symbol)
    if constructor:
        for function in constructor:
            print("%s %s -> %s" % (initializer, symbol, function))
    else:
        print("%s %s" % (initializer, symbol))

C++静态初始化器不是直接调用的,而是通过两个生成的函数_GLOBAL__sub_I_..__static_initialization..来调用。该脚本使用这些函数的反汇编来获取实际构造函数的名称。您需要使用c++filt工具来解除名称修饰,或者从脚本中删除调用以查看原始符号名称。

共享库可以有自己的初始化器列表,这些列表不会被此脚本显示。在那里情况稍微复杂一些:对于非静态初始化器,.init_array得到一个全零条目,在加载库时将其覆盖为初始化器的最终地址。因此,此脚本将输出一个带有所有零的地址。


谢谢!但这样做相比于“nm -a binaryOrSharedLibrary | grep GLOBAL__”,有什么优势呢?对我来说,它更简单,并且生成了相同的输出。获得符号并不那么困难。相反,我想知道文件/行,以便找到我需要编辑的内容。 - milianw
符号通常在非调试版本中被剥离。这也会移除 GLOBAL__ 符号。另一方面,.init_array 通常是存在的。我更喜欢使用 .init_array 的第二个原因是它更通用:C 初始化函数(具有 __attribute__((constructor)) 的函数)也使用此机制,但没有为它们生成单独的 GLOBAL__ 符号。至于符号名称,有一个生成的中间函数。在 objdump -d 中搜索 __static_initialization_and_destruction_ - Phillip
太好了,查看__static_initialization_and_destruction_的反汇编输出的提示真的很有帮助。有了这个,我可以编写一个脚本,将_GLOBAL__sub_I_filename.cpp映射到我从__static_initialization_and_destruction_中的callq指令获取的标识符!当然,这只适用于非剥离的完整调试无内联构建的共享库。但对于我的用例来说已经足够好了! - milianw
我已经更新了脚本,以便跟随生成的C++代码到实际构造函数。 - Phillip
请注意,(a) __static_initialization... 很可能会被内联到 _GLOBAL 函数中,(b) 例如 clang 使用不同的名称来命名各个构造函数包装器。 - Trass3r
显示剩余6条评论

1

加载 ELF 对象时会执行多个操作,不仅仅是 .init_array。为了了解整体情况,建议查看 libc loader 的源码,特别是 _dl_init()call_init()


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接