c - 函数 "main"的代码在目标文件中的哪里开始?

标签 c assembly memory gdb elf

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

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

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

此外,我认为我们应该将符号表中直到 main 为止的所有行的大小相加,但我根本不确定它是否正确。

后续的另一个问题是我是否想用 NOP 指令替换 main 函数或在我的目标文件中植入一个 ret 指令。我怎样才能知道可以使用十六进制编辑器执行此操作的偏移量。

最佳答案

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

从这个 C 文件开始:

#include <stdio.h>

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

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

由于注释看起来像是在 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 文件)和调试器的详细形式,第二次是内核用于加载程序的压缩形式。详细形式是部分列表,由部分标题组成,您可以像这样查看它(输出被缩短了一点,以使答案更具可读性):

$ 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 的地址中减去文本部分的基址,您会发现 main 占文本部分的 13d 字节。节列表还包含文本节数据开始的文件偏移量。它是 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
$

使用 x86 上 return(near)的操作码 0xc3 覆盖 main 的第一个字节,导致程序不再输出任何内容。

关于c - 函数 "main"的代码在目标文件中的哪里开始?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62088056/

相关文章:

java - 尽管指定了正确的结构,为什么我会获得无效的内存访问?

delphi - 为什么这个 LEA 指令不能编译?

c++ - 删除后指针指向哪里?

sql-server - 如果子查询返回太多数据,我应该担心吗?

python - 在sklearn python中拟合GaussianMixture时如何处理内存错误?

android - 无法定位符号

c - 如何在一个以指针为参数的函数中访问另一个以指针为参数的函数中的值?

c - 什么定义了类型的大小?

assembly - 检测IP之前的调用指令

assembly - 打印字符串中的 x 字符 (MIPS)