【问题标题】:Where in object file does the code of function "main" starts?函数“main”的代码在目标文件中的什么位置开始?
【发布时间】:2020-09-17 03:31:33
【问题描述】:

我有一个 C 程序的目标文件,它打印 hello world,只是为了这个问题。 我正在尝试使用 readelf 实用程序或 gdb 或 hexedit(我无法确定哪个工具是正确的)来了解文件中函数“main”的代码在哪里开始。

我知道使用 readelf 会出现符号 _start & main 以及它在虚拟内存中映射的地址。此外,我还知道 .text 部分的大小和指定入口点的 coruse 的大小,即与 text 部分相同的地址。

问题是 - 函数“main”的代码在文件中的什么位置开始?我认为这是文本部分的入口点和偏移量,但我如何理解它的部分数据,bss,rodata 应该在 main 之前运行,并且它出现在 readelf 中的部分文本之后。

另外我认为我们应该对符号表中所有行的大小求和,但我完全不确定它是否正确。

这个问题的后续问题是我是否想用 NOP 指令替换主函数或在我的目标文件中植入一条 ret 指令。我如何知道使用 hexedit 可以做到的偏移量。

【问题讨论】:

  • 你可以只看章节标题。找出哪个段包含符号的虚拟地址(应该是.text)然后减去该段的起始地址,最后加上文件偏移量。
  • @Jester 我不确定我是否理解公式。我的地址.text 部分的部分与入口点相同,(例如 0x80482e0 和 .text 部分的部分标题中的偏移量为 0002e0。然后你的意思是从入口点减去它,相同的,我们得到 0 并添加偏移量,我们得到2e0. 是正确的还是我错过了什么?
  • 是的,这只是一个快乐的小意外 (TM),因为您的符号正好位于一节的开头,因此您可以直接使用偏移量。
  • 这些部分在内存或文件中的顺序无关紧要。在代码开始在给定入口点执行之前,所有可加载部分(实际上是段)都已映射。从历史上看,您希望 .data.bss 在内存布局的末尾,这样您就可以使用 brk 来增加堆。
  • 使用带有反汇编选项的 objdump,也许还有其他选项。

标签: c assembly memory gdb elf


【解决方案1】:

那么,让我们一步一步来吧。

从这个 C 文件开始:

#include <stdio.h>

void printit()
{
    puts("Hello world!");
}

int main(void)
{
    printit();
    return 0;
}

因为 cmets 看起来像是在 x86 上,所以将其编译为 32 位非 PIE 可执行文件,如下所示:

$ gcc -m32 -no-pie  -o test test.c

-m32 选项是必需的,因为我在 x86-64 机器上工作。如您所知,您可以使用 readelf、objdump 或 nm 来获取 main 的虚拟内存地址,例如:

$ nm test | grep -w main
0804918d T main

显然,804918d 不能是文件中只有 15 kB 大的偏移量。您需要找到虚拟内存地址文件偏移量之间的映射。在典型的 ELF 文件中,映射被包含两次。一次是链接器(因为目标文件也是 ELF 文件)和调试器的详细形式,第二次是内核用于加载程序的压缩形式。详细的形式是section的列表,由section header组成,可以这样查看(输出缩短了一点,让答案更具可读性):

$ readelf --section-headers test
There are 29 section headers, starting at offset 0x3748:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
[...]
  [11] .init             PROGBITS        08049000 001000 000020 00  AX  0   0  4
  [12] .plt              PROGBITS        08049020 001020 000030 04  AX  0   0 16
  [13] .text             PROGBITS        08049050 001050 0001c1 00  AX  0   0 16
  [14] .fini             PROGBITS        08049214 001214 000014 00  AX  0   0  4
  [15] .rodata           PROGBITS        0804a000 002000 000015 00   A  0   0  4
[...]
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

在这里您发现.text 部分从(虚拟)地址08049050 开始,大小为1c1 字节,因此它以地址08049211 结束。 main的地址804918d在这个范围内,所以你知道main是文本部分的成员。如果你从 main 的地址中减去 text 部分的基数,你会发现 main 是 13d 字节进入 text 部分。节列表还包含文本节数据开始的文件偏移量。它是1050,所以main 的第一个字节在偏移量0x1050 + 0x13d == 0x118d

您可以使用程序头进行相同的计算:

$ readelf --program-headers test
[...]
Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x00160 0x00160 R   0x4
  INTERP         0x000194 0x08048194 0x08048194 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x08048000 0x08048000 0x002e8 0x002e8 R   0x1000
  LOAD           0x001000 0x08049000 0x08049000 0x00228 0x00228 R E 0x1000
  LOAD           0x002000 0x0804a000 0x0804a000 0x0019c 0x0019c R   0x1000
  LOAD           0x002f0c 0x0804bf0c 0x0804bf0c 0x00110 0x00114 RW  0x1000
[...]

第二个加载行告诉您08049000 (VirtAddr) 到08049228 (VirtAddr + MemSiz) 的区域是可读和可执行的,并且从文件中的偏移量1000 加载。所以你可以再次计算出 main 的地址是 18d 字节到这个加载区域,所以它必须驻留在可执行文件内的偏移量 0x118d 处。让我们测试一下:

$ ./test
Hello world!
$ echo -ne '\xc3' | dd of=test conv=notrunc bs=1 count=1 seek=$((0x118d))
1+0 records in
1+0 records out
1 byte copied, 0.0116672 s, 0.1 kB/s
$ ./test
$

0xc3 覆盖main 的第一个字节,x86 上返回(near)的操作码,导致程序不再输出任何东西。

【讨论】:

  • 明白。谢谢!或者,我可以放置 NOP 指令而不覆盖 ret。如果起始地址是 118d,那么我们应该在 C3 操作码之前放置 NOP 直到(118d + main 函数的大小)。对吗?
  • 你不能确定 main 的最后一个操作码总是 ret 指令。如果存在编译器认为不太可能执行的 if 块,编译器可能会将其代码放在 RET 操作码之后。当然,在第一个 C3 字节之前,您可以 NOP 输出所有内容。即使 C3 实际上是操作数或地址的一部分,它也会起作用。
  • 另一方面,main 中甚至根本不需要 RET 指令。如果编译器可以在所有代码路径上应用尾调用优化,它总是会跳出 main。此外,如果 main 调用已知不会返回的函数,编译器可能会停止编译这些调用之后的代码。
【解决方案2】:

_start 通常属于一个固定的模块(*.o 文件)(在不同的系统上调用不同,但通用名称是 crt0.o,它是用汇编程序编写的。)固定的代码准备堆栈(通常参数和环境通过execve(2)系统调用存储在初始堆栈段中)crt0.s的任务是准备初始C堆栈帧并调用main()。一旦main()结束,它负责从main中获取返回值,并调用所有atexit()处理函数来完成对_exit(2)系统调用的调用。

crt0.o 的链接通常是透明的,因为您总是调用编译器来自己进行链接,因此您通常不必添加 crt0.o 作为第一个对象模块,但编译器知道(最近,所有这些东西都大大增加了,因为我们依赖架构和 ABI 在函数之间传递参数)

如果您使用-v 选项执行编译器,您将获得它用于调用链接器的确切命令行,并且您将获得程序在其第一阶段所具有的最终内存映射的秘密。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-05-15
    • 1970-01-01
    • 2018-11-12
    • 1970-01-01
    • 1970-01-01
    • 2012-12-03
    相关资源
    最近更新 更多