objcopy -O binary
复制源文件的内容。这里test.o
是“可重定位目标文件”:即代码,也是符号表和重定位信息,它允许文件与其他文件链接成可执行程序。生成的test.bin
文件objcopy
只包含代码,没有符号表或重定位信息。这样的“原始”文件对于“普通”编程是无用的,但对于有自己的加载器的代码来说很方便。
我假设您在 32 位 x86 系统上使用 Linux。您的test.o
文件大小为 515 字节。如果您尝试objdump -x test.o
,您会得到以下内容,其中描述了test.o
目标文件的内容:
$ objdump -x test.o
test.o: file format elf32-i386
test.o
architecture: i386, flags 0x00000010:
HAS_SYMS
start address 0x00000000
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000001e 00000000 00000000 00000034 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000000 00000000 00000000 00000054 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 00000000 00000000 00000054 2**2
ALLOC
SYMBOL TABLE:
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
0000000b l .text 00000000 start
00000005 l .text 00000000 str
这为您提供了很多信息。特别是,该文件包含一个称为.text
从文件中偏移量 0x34 开始(十进制为 52)和长度为 0x1e 字节(十进制为 30)的部分。您可以反汇编它以查看操作码本身:
$ objdump -d test.o
test.o: file format elf32-i386
Disassembly of section .text:
00000000 <str-0x5>:
0: e8 06 00 00 00 call b <start>
00000005 <str>:
5: 74 65 je 6c <start+0x61>
7: 73 74 jae 7d <start+0x72>
9: 0a 00 or (%eax),%al
0000000b <start>:
b: b8 04 00 00 00 mov $0x4,%eax
10: bb 01 00 00 00 mov $0x1,%ebx
15: 59 pop %ecx
16: ba 05 00 00 00 mov $0x5,%edx
1b: cd 80 int $0x80
1d: c3 ret
这或多或少是您开始使用的程序集。中间的je
,jae
和or
操作码是虚假的:这是objdump
试图将文字字符串 ( "test\n"
,导致字节 0x74 0x65 0x73 0x64 0x0a 0x00) 解释为操作码。objdump -d
还显示了在该部分中找到的实际字节.text
,即文件中从偏移量 0x34 开始的字节。第一个字节是 0xe8 0x06 0x00...
现在,看看test.bin
文件。它的长度为 30 个字节。让我们以十六进制查看这些字节:
$ hd test.bin
00000000 e8 06 00 00 00 74 65 73 74 0a 00 b8 04 00 00 00 |.....test.......|
00000010 bb 01 00 00 00 59 ba 05 00 00 00 cd 80 c3 |.....Y........|
我们在这里准确地识别.text
出test.o
. 就是这样objcopy -O binary
做的:它提取了文件内容,即唯一的非空部分,即原始操作码本身,删除了其他所有内容,特别是符号表和重定位信息。
重定位是关于在给定代码段中必须更改的内容,以便它在存储在内存中的给定位置时正常运行。例如,如果代码使用一个变量并希望获得该变量的地址,那么重定位信息将包含一个条目,告诉将代码实际放置在内存中的任何人(通常是链接器):“在代码中,当你知道变量实际在哪里时,写下变量地址”。有趣的是,您显示的代码不需要重定位:字节序列可以写入任意内存位置并按原样执行。
让我们看一下代码的作用。
call
操作码跳转到mov
偏移量 0x0b 处的指令。此外,由于这是 a call
,它会将返回地址压入堆栈。返回地址是调用完成后应继续执行的位置,即ret
到达操作码时。这是操作码后面的字节地址call
。在这里,该地址是文字字符串的第一个字节的地址"test\n"
。
- 这两个
movl
负载%eax
和%ebx
数值分别为 4 和 1。
- 操作码从堆栈中
pop
删除顶部元素,将其存储在%ecx
. 这个顶级元素是什么?这正是call
操作码压入堆栈的地址,即文字字符串的第一个字节的地址。
- 第三个
movl
加载%edx
数值 5。
int $0x80
是 32 位 x86 Linux 上的系统调用:它调用内核。内核将查看寄存器以知道该做什么。内核首先查看%eax
获取“系统调用号”;在 32 位 x86 上,“4”是__NR_write
,即write()
系统调用。%ebx
此调用需要三个参数,依次位于寄存器%ecx
和%edx
中。这些是目标文件描述符(此处为 1:这是标准输出)、指向要写入的数据的指针(此处为文字字符串)和要写入的数据的长度(此处为 5,对应于四个字母和换行符特点)。所以这写"test\n"
在标准输出上。
- 最后
ret
返回给调用者。ret
从堆栈中弹出一个值,然后跳转到该地址。这假定此代码块是使用call
操作码调用的。
因此,总而言之,代码test
以换行符打印出来。
让我们用一个自定义加载器试试:
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
int
main(void)
{
void *p;
int f;
p = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
f = open("test.bin", O_RDONLY);
read(f, p, 30);
close(f);
mprotect(p, 30, PROT_READ | PROT_EXEC);
((void (*)(void))p)();
return 0;
}
(上面的代码没有测试返回值是否有错误,这当然很糟糕。)
在这里,我用 分配了一个内存页面(4096 字节)mmap()
,请求一个我可以读写的页面。p
指向那个块。然后,使用open()
,read()
和close()
,我将test.bin
文件的内容(30 字节)读入该块。
该mprotect()
调用指示内核更改我的页面的访问权限:现在,我希望能够执行这些字节,即将它们视为机器代码。我放弃了写入块的权利(取决于确切的内核配置,可能禁止拥有一个既可以写入又可以执行的页面)。
神秘的((void (*)(void))p)();
读法是这样的:我接受p
;我将它转换为指向不带参数且不返回任何函数的函数的指针;我调用那个函数。call
这是用于将 a放入我的数据块的C 语法。
当我运行该程序时,我得到:
$ ./blah
test
这是预期的:在标准输出上test.bin
写出的代码。test