函数 "main" 的代码在目标文件中的哪里开始?
Where in object file does the code of function "main" starts?
我有一个打印 hello world 的 C 程序的目标文件,只是为了这个问题。
我试图了解使用 readelf 实用程序或 gdb 或 hexedit(我不知道哪个工具是正确的)在文件中函数代码 "main" 开始的位置。
我知道使用 readelf 符号 _start & main 出现以及它在虚拟内存中映射的地址。此外,我还知道 .text 部分的大小和指定入口点的 coruse 的大小,即与文本部分相同的地址。
问题是 - 文件中函数 "main" 的代码从哪里开始?我认为那是文本部分的入口点和偏移量,但我如何理解它,部分数据、bss、rodata 应该在 main 之前 运行 并且它出现在 readelf 中的部分文本之后。
我还认为我们应该将所有行的大小加起来,直到符号 table 中的主要内容,但我完全不确定它是否正确。
跟进这个问题的另一个问题是,如果我想用 NOP 指令替换 main 函数,或者在我的目标文件中添加一个 ret 指令。我怎样才能知道我可以使用 hexedit 执行的偏移量。
那么,让我们一步一步来吧。
从这个 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 文件)和调试器的详细形式,第二次是内核用于加载程序的压缩形式。详细的形式是sections的列表,由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
是text段的成员。如果从 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 上 return(near)的操作码,导致程序不再输出任何内容。
_start
通常属于一个固定的模块(一个 *.o
文件)(在不同的系统上叫法不同,但是一个通用的名字是 crt0.o
写在汇编程序。)固定代码准备堆栈(通常参数和环境通过 execve(2)
系统调用存储在初始堆栈段中)crt0.s
的任务是准备初始 C 堆栈帧和打电话 main()
。一旦 main()
结束,它负责从 main 获取 return 值并调用所有 atexit()
处理程序以完成调用 _exit(2)
系统调用。
crt0.o
的链接通常是透明的,因为您总是调用编译器自己进行链接,因此您通常不必将 crt0.o
添加为第一个对象模块,但编译器知道(最近,所有这些东西都有了很大的增长,因为我们依赖架构和 ABI 在函数之间传递参数)
如果您使用 -v
选项执行编译器,您将获得它用于调用链接器的确切命令行,并且您将获得程序在其上的最终内存映射的秘密第一阶段。
我有一个打印 hello world 的 C 程序的目标文件,只是为了这个问题。 我试图了解使用 readelf 实用程序或 gdb 或 hexedit(我不知道哪个工具是正确的)在文件中函数代码 "main" 开始的位置。
我知道使用 readelf 符号 _start & main 出现以及它在虚拟内存中映射的地址。此外,我还知道 .text 部分的大小和指定入口点的 coruse 的大小,即与文本部分相同的地址。
问题是 - 文件中函数 "main" 的代码从哪里开始?我认为那是文本部分的入口点和偏移量,但我如何理解它,部分数据、bss、rodata 应该在 main 之前 运行 并且它出现在 readelf 中的部分文本之后。
我还认为我们应该将所有行的大小加起来,直到符号 table 中的主要内容,但我完全不确定它是否正确。
跟进这个问题的另一个问题是,如果我想用 NOP 指令替换 main 函数,或者在我的目标文件中添加一个 ret 指令。我怎样才能知道我可以使用 hexedit 执行的偏移量。
那么,让我们一步一步来吧。
从这个 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 文件)和调试器的详细形式,第二次是内核用于加载程序的压缩形式。详细的形式是sections的列表,由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
是text段的成员。如果从 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 上 return(near)的操作码,导致程序不再输出任何内容。
_start
通常属于一个固定的模块(一个 *.o
文件)(在不同的系统上叫法不同,但是一个通用的名字是 crt0.o
写在汇编程序。)固定代码准备堆栈(通常参数和环境通过 execve(2)
系统调用存储在初始堆栈段中)crt0.s
的任务是准备初始 C 堆栈帧和打电话 main()
。一旦 main()
结束,它负责从 main 获取 return 值并调用所有 atexit()
处理程序以完成调用 _exit(2)
系统调用。
crt0.o
的链接通常是透明的,因为您总是调用编译器自己进行链接,因此您通常不必将 crt0.o
添加为第一个对象模块,但编译器知道(最近,所有这些东西都有了很大的增长,因为我们依赖架构和 ABI 在函数之间传递参数)
如果您使用 -v
选项执行编译器,您将获得它用于调用链接器的确切命令行,并且您将获得程序在其上的最终内存映射的秘密第一阶段。