|
使用GCC生成无格式二进制文件(plain binary files)XML:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" /> 我在互联网上搜索很久,只找到一些零星的关于这方面的资料。我想使用gcc开发一个自己使用的专用工具,结合自己的工作经验,写了这篇总结性的资料。 1. 软硬件环境 l 至少一台正在使用的80x86系列的32-bit电脑,越高档越好。 l 一套Linux的发行版,如Redhat、Mandrake、TurboLinux等。 l GNU GCC编译器。该编译器在Linux下很常用。 l Linux上的binutils。 l 自己熟悉的文本编辑器,如vi等。 如果你不具备这些条件,就不要再往下看了。我的工作环境是,在一台赛扬433上安装了Redhat Linux8.0,128M内存,gcc是默认的,版本为3.2.2。可以使用如下命令查看gcc的版本: 2. 使用C语言生成一个二进制文件 使用自己喜欢的文本编辑器写一个test.c: 再使用如下命令编译: | gcc –c test.c ld –o test –Ttext 0x0 –e main test.o objcopy –R .note –R .comment –S –O binary test test.bin | 最后生成的二进制文件是test.bin,可以使用你喜欢的反汇编工具看看这个文件里到底是什么。我使用Linux下的objdump进行反汇编: | objdump –D –b binary –a i386 test.bin | 结果如下: | 00000000 <main>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: c9 leave 11: c3 ret | 其中第一列是指令的内存地址;第二列是指令的机器码;第三列是汇编指令。相信你的结果与此同。如果你的gcc与我的不一样,例如2.7.x版本的gcc,你的结果很可能会有所不同,缺少如下的四条指令,这是正常的,这两个版本的gcc所使用的堆栈框架不同(下面介绍的例子也会因为编译器版本的不同造成其结果有别): | 3: 83 ec 08 sub $0x8,%esp 6: 83 e4 f0 and $0xfffffff0,%esp #堆栈对齐,以16Bytes为单位分配局部变量空间 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp | 上述代码都是32-bit代码,你需要在像Linux这样的 32-bit环境下运行,并且是保护模式。也可以只用下面的指令直接生成test.bin: | gcc –c test.c ld –Ttext 0x0 –e main --oformat binary –o test.bin test.o | 上面的test.c中只有一个函数,而且还只是个框架。其反汇编代码也没什么难理解的。 3. 编写带局部变量的程序 再创建一个新的test.c,看看gcc是如何处理局部变量的。 | int main() { int i; i=0x12345678; } | 使用上述两种方法的人一种编译,生成test.bin。然后使用objdump进行反汇编: | 00000000 <main>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: c7 45 fc 78 56 34 12 movl $0x12345678,0xfffffffc(%ebp) 17: c9 leave 18: c3 ret | 与第一个例子相比,开头的六条指令和最后的两条指令完全相同,仅有一条指令不同。这条语句是给局部变量赋值,其空间的分配在前面已经进行了。在gcc中,堆栈中的局部变量空间按16字节为单位进行分配,而不是通常的1字节为单位。如果将 | int i; i=0x12345678; 改为 int i=0x12345678; | 其结果没有区别。但是,如果是全局变量,就不一样了。 4. 编写带全局变量的程序 将test.c改为: | int i; int main() { i=0x12345678; } | 使用同样的方法编译,然后再进行反汇编: | 00000000 <main>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: c7 05 1c 10 00 00 78 movl $0x12345678,0x101c 17: 56 34 12 1a: c9 leave 1b: c3 ret | 我们定义的全局变量被放到了0x101c处,这是gcc默认以page-align对齐数据段的结果,此处的page与页式内存管理中的page没有关系。在使用ld链接时,使用-N参数可以关闭对齐效果。 | 00000000 <main>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: c7 05 1c 00 00 00 78 movl $0x12345678,0x1c 17: 56 34 12 1a: c9 leave 1b: c3 ret | 正如我们看到的,数据段紧接着代码段。我们也可以明确的指定数据段的位置,试试下面的命令再进行编译: | gcc –c test.c ld –Ttext 0x0 –Tdata 0x1234 –e main –N --oformat binary –o test.bin test.o | 然后再使用objdump进行反汇编: | 00000000 <.data>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: c7 05 34 12 00 00 78 movl $0x12345678,0x1234 17: 56 34 12 1a: c9 leave 1b: c3 ret | 现在,我们定义的全局变量被放到0x1234处了。通过给ld指定-Tdata参数,可以自由的定义数据段的地址,如果不指定,数据段在代码段后。 再看看直接给全局变量进行初始化的情况。 | const int I=0x12345678; int main() { } | 仍然使用上面的方法进行编译、链接、反汇编,其结果如下: | 00000000 <.data>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: c9 leave 11: c3 ret 12: 00 00 add %al,(%eax) 14: 78 56 js 0x6c 16: 34 12 xor $0x12,%al | 代码以4Bytes对齐,全局变量被直接存储在代码段之后的数据段,ld直接将常数放到了全局变量的位置,一步到位。 使用如下命令可以看到更多细节: 可以看到如下的结果: | test.o: file format elf32-i386 Disassembly of section .text: 00000000 <main>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: c9 leave 11: c3 ret Disassembly of section .data: Disassembly of section .rodata: 00000000 <i>: 0: 78 56 js 58 <main+0x58> 2: 34 12 xor $0x12,%al | 我们可以更清楚地看到,在.c文件中定义的全局常量被放在了只读的数据段中了。再看下面的一段代码: | int I=0x12345678; const int c=0x12345678; int main() { } | 还是使用上面的方法编译、链接、反汇编,可以到到如下结果: | test.o: file format elf32-i386 Disassembly of section .text: 00000000 <main>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: c9 leave 11: c3 ret Disassembly of section .data: 00000000 <i>: 0: 78 56 js 58 <main+0x58> 2: 34 12 xor $0x12,%al Disassembly of section .rodata: 00000000 <c>: 0: 78 56 js 58 <main+0x58> 2: 34 12 xor $0x12,%al | 可以看出,整数I被放在了普通的数据段中,常数c被放在了只读数据段中了。当使用全局变量(常量)时,ld会自动的使用合适的数据段存储他们。 5. 处理指针 使用如下代码来查看gcc处理指针变量的情况: | int main() { int I; int* p; p=&I; *p=0x12345678; } | 使用objdump查看生成的机器代码: | 00000000 <main>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 08 sub $0x8,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: 8d 45 fc lea 0xfffffffc(%ebp),%eax 13: 89 45 f8 mov %eax,0xfffffff8(%ebp) 16: 8b 45 f8 mov 0xfffffff8(%ebp),%eax 19: c7 00 78 56 34 12 movl $0x12345678,(%eax) 1f: c9 leave 20: c3 ret | 一开始,gcc已经为局部变量预分配了至少8Bytes的空间,并且使esp以16Bytes边界对齐,如果还需要额外的空间,gcc将按照16Bytes为单位进行分配,而不是其他编译器所使用的以1Byte为单位进行分配。变量I位于ebp-4,变量p位于ebp-8,lea指令将I的有效地址放入eax中,然后又被放入 |