wordpress la,国外seo工具,wordpress 千万级数据,怎么查看网站开发人使用C语言生成一个二进制文件
使用自己喜欢的文本编辑器写一个test.c#xff1a; int main() { }
再使用如下命令编译#xff1a; gcc –c test.c ld –o test –Ttext 0x0 –e main test.o objcopy –R .note –R .comment –S –O binary test test.bin
最后生成的二进…使用C语言生成一个二进制文件
使用自己喜欢的文本编辑器写一个test.c int main() { }
再使用如下命令编译 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; i0x12345678; }
使用上述两种方法的人一种编译生成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; i0x12345678; 改为 int i0x12345678;
其结果没有区别。但是如果是全局变量就不一样了。
4. 编写带全局变量的程序
将test.c改为 int i; int main() { i0x12345678; }
使用同样的方法编译然后再进行反汇编 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 I0x12345678; 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直接将常数放到了全局变量的位置一步到位。
使用如下命令可以看到更多细节 objdump –D test.o
可以看到如下的结果 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 main0x58 2: 34 12 xor $0x12,%al
我们可以更清楚地看到在.c文件中定义的全局常量被放在了只读的数据段中了。再看下面的一段代码 int I0x12345678; const int c0x12345678; 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 main0x58 2: 34 12 xor $0x12,%al Disassembly of section .rodata: 00000000 c: 0: 78 56 js 58 main0x58 2: 34 12 xor $0x12,%al
可以看出整数I被放在了普通的数据段中常数c被放在了只读数据段中了。当使用全局变量常量时ld会自动的使用合适的数据段存储他们。
5. 处理指针
使用如下代码来查看gcc处理指针变量的情况 int main() { int I; int* p; pI; *p0x12345678; }
使用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-8lea指令将I的有效地址放入eax中然后又被放入p中。最后将0x12345678赋给p指向的变量I。
6. 关于函数调用
看如下代码 void func(); int main() { func(); } void func() { }
再看生成的二进制代码 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: e8 03 00 00 00 call 0x18 15: c9 leave 16: c3 ret 17: 90 nop 18: 55 push %ebp 19: 89 e5 mov %esp,%ebp 1b: c9 leave 1c: c3 ret
主函数main通过call指令调用了空函数func该函数与main大同小异。为ld指定-Map开关输出map文件可以得到更详细的信息。 .text 0x00000000 0x1d *(.text .stub .text.* .gnu.linkonce.t.*) .text 0x00000000 0x1d test.o 0x00000000 main 0x00000018 func
第一列是段名这里是.text第二列是起始位置第三列是段长度最后一列是附加信息如函数名、所出自的目标文件等。可以看到.text段从0x0开始长度为0x1d函数func从0x18开始。
7. 函数的返回值
看下面的代码主函数main返回一个整型值 int main() { return 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: b8 78 56 34 12 mov $0x12345678,%eax 15: c9 leave 16: c3 ret
你已经看到了gcc使用eax传递返回值。因为返回值就是eax寄存器的值所以你可以隐含的返回甚至什么都不返回。因为返回值保存在寄存器中进行函数调用时经常忽略返回值。例如我们经常这样调用函数 printf(…);
该函数是有返回值的。如果函数返回的数据大于4Bytes就不能再使用这种方法返回数据了。再看下面的例子 typedef strUCt mydef{ int a,b,c,d; int array[10]; }mydef; mydef func(); int main() { mydef d; dfunc(); } mydef func() { mydef d; return d; }
接着看反汇编的代码 00000000 .data: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 48 sub $0x48,%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 b8 lea 0xffffffb8(%ebp),%eax 13: 83 ec 0c sub $0xc,%esp 16: 50 push %eax 17: e8 06 00 00 00 call 0x22 1c: 83 c4 0c add $0xc,%esp 1f: c9 leave 20: c3 ret 21: 90 nop 22: 55 push %ebp 23: 89 e5 mov %esp,%ebp 25: 57 push %edi 26: 56 push %esi 27: 83 ec 40 sub $0x40,%esp 2a: 8b 7d 08 mov 0x8(%ebp),%edi 2d: 8d 75 b8 lea 0xffffffb8(%ebp),%esi 30: fc cld 31: b8 0e 00 00 00 mov $0xe,%eax 36: 89 c1 mov %eax,%ecx 38: f3 a5 repz movsl %ds:(%esi),%es:(%edi) 3a: 8b 45 08 mov 0x8(%ebp),%eax 3d: 83 c4 40 add $0x40,%esp 40: 5e pop %esi 41: 5f pop %edi 42: c9 leave 43: c2 04 00 ret $0x4
我们自定义的结构为0x38Bytesgcc为了保持堆栈的16Bytes对齐分配了0x40Bytes的空间。函数func并没有参数但是在调用时却将变量d的指针传了进去。然后利用这个指针使用指令movsl直接对d进行赋值。再看下面的例子 typedef struct mydef{ int a,b,c,d; int array[10]; }mydef; mydef func(); int main() { func(); } mydef func() { mydef d; return d; }
再看反汇编的结果 00000000 .data: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 48 sub $0x48,%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 b8 lea 0xffffffb8(%ebp),%eax 13: 83 ec 0c sub $0xc,%esp 16: 50 push %eax 17: e8 06 00 00 00 call 0x22 1c: 83 c4 0c add $0xc,%esp 1f: c9 leave 20: c3 ret 21: 90 nop 22: 55 push %ebp 23: 89 e5 mov %esp,%ebp 25: 57 push %edi 26: 56 push %esi 27: 83 ec 40 sub $0x40,%esp 2a: 8b 7d 08 mov 0x8(%ebp),%edi 2d: 8d 75 b8 lea 0xffffffb8(%ebp),%esi 30: fc cld 31: b8 0e 00 00 00 mov $0xe,%eax 36: 89 c1 mov %eax,%ecx 38: f3 a5 repz movsl %ds:(%esi),%es:(%edi) 3a: 8b 45 08 mov 0x8(%ebp),%eax 3d: 83 c4 40 add $0x40,%esp 40: 5e pop %esi 41: 5f pop %edi 42: c9 leave 43: c2 04 00 ret $0x4
可以说与上面的结果一字不差我们没有在main函数中声明变量存储func返回的结果但是gcc替我们做了。它仍然为函数func传递了一个指针并将结果传了出来尽管我们对返回值不感兴趣但编译器对我们的兴趣好像也没有兴趣依然我行我素。如果使用了优化选项结果很可能有所相同。
8. 给函数传递参数
gcc遵循一般的c语言标准包括参数传递方式。看看下面的例子 char res; char func(char a,char b); int main() { resfunc(0x02,0x03); } char func(char a,char b) { return ab; }
再看看他的反汇编代码 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: 83 ec 08 sub $0x8,%esp 13: 6a 03 push $0x3 15: 6a 02 push $0x2 17: e8 0a 00 00 00 call 0x26 1c: 83 c4 10 add $0x10,%esp 1f: a2 44 00 00 00 mov %al,0x44 24: c9 leave 25: c3 ret 26: 55 push %ebp 27: 89 e5 mov %esp,%ebp 29: 83 ec 04 sub $0x4,%esp 2c: 8b 45 08 mov 0x8(%ebp),%eax 2f: 8b 55 0c mov 0xc(%ebp),%edx 32: 88 45 ff mov %al,0xffffffff(%ebp) 35: 88 55 fe mov %dl,0xfffffffe(%ebp) 38: 8a 45 fe mov 0xfffffffe(%ebp),%al 3b: 02 45 ff add 0xffffffff(%ebp),%al 3e: 0f be c0 movsbl %al,%eax 41: c9 leave 42: c3 ret
如果你精通汇编语言看完这段代码恐怕你已经口吐鲜血并晕倒在地了gcc居然生成了这么啰嗦的代码但是我们还是先说说C语言的函数调用规范吧。
我们已经看到了参数从右到左依次入栈。下面的说明全部以32Bytes代码为准其规范具体可罗列以下几条
l 调用者负责将参数压入堆栈顺序为从右到左依次入栈。也就是左边的最后入栈。
l 调用者使用near call指令将控制权传给被调用者。
l 被调用者得到控制权一般需要创建堆栈框架这不是必需的通常都是这么做。首先将ebp压入堆栈保存再将esp放入ebp使ebp成为访问参数的基址指针。
l 被调用者通过ebp访问参数。因为ebp已经先行压入堆栈所以[ebp4]就是被call指令自动压入堆栈的返回地址显然从[ebp8]开始就是参数。由于函数最左边的参数最后被压入堆栈所以[ebp8]就是该参数其他参数以此类推。像printf这样的函数具有个数不确定的参数但是参数入栈顺序的规则说明被调用者通过[ebp8]就能够找到第一个参数其他参数的类型和数目则需要由第一个参数给出。
l 被调用者减小esp的值为堆栈中的临时变量分配空间然后使用ebp和一个负的偏移访问。
l 被调用者使用alaxeax返回大小不同的值。浮点数可以通过ST0寄存器返回。
l 被调用者完成处理后使用事先建立的堆栈框架恢复espsbp的值并使用ret指令返回调用者。
l 调用者重新得到控制权通过给esp加上一个立即数清空堆栈尽量不要使用多次pop指令清空堆栈。如果因为使用了错误的函数原型通过堆栈多传递了或者少传递了参数调用者仍然能够将堆栈恢复到正确的状态因为调用者知道自己向堆栈压了几个字节的数据。
结合C语言的函数调用规则上面的代码不难理解。
从80386开始push指令的操作数可以是8-bit16-bit32-bit但是C语言统统按32-bit整型数处理被调用者也按32-bit进行处理。这一点很重要特别是汇编语言和C语言混合编程时。
9. 基本的数据类型间的转换
gcc处理三类基本数据类型
l signed char , unsigned char , 1 Byte
l signed short , unsigned short , 2 Bytes
l signed int , unsigned int , 4 Bytes
各种数据类型间的转换遵循一般C语言的规则具体可以参考IA-32的标准。这里只举一例说明 int main() { char ch’a’; int x2; int y-4; }
使用同样的方法进行编译及反汇编 00000000 main: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 18 sub $0x18,%esp 6: 83 e4 f0 and $0xfffffff0,%esp 9: b8 00 00 00 00 mov $0x0,%eax e: 29 c4 sub %eax,%esp 10: c6 45 ff 61 moVB $0x61,0xffffffff(%ebp) 14: c7 45 f8 02 00 00 00 movl $0x2,0xfffffff8(%ebp) 1b: c7 45 f4 fc ff ff ff movl $0xfffffffc,0xfffffff4(%ebp) 22: c9 leave 23: c3 ret 10. gcc编译代码的基本运行环境
这一部分我查了很多的文档都没有这方面的介绍。又请教了很多的高手大致情况如下我实在无法保证这里所说的都是正确的并且将来也是正确的仅供参考
l 32-bit的保护模式下运行。
l 段寄存器CSdsesfsgsss必须指向同一段内存区域。
l 没有初始化的全局变量被放在BSS的段内该区域在代码段之后。但是如果你生成的文件是二进制文件BSS段不是该文件的一部分你需要自己小心使用。初始化的全局变量在DATA段内它是二进制文件的一部分并且位于代码段之后。被声明为const的全局变量被放在RODATA段内它也是二进制文件的一部分并放在代码段之后。
l 确保堆栈没有溢出小心代码段和全局数据不要被破坏。
我也查了Intel提供的帮助文档“Intel Architecture Software Developer’s Manual”一共有三卷之多参考了其中关于内存组织Volume 1:Memory Organization中的说法建议你去好好研究。总之使csdsss总是指向同一内存区域应该可以使代码正确运行。如果运行环境不是这样我就不知道结果了。
11. 访问外部的全局变量
看看在非C语言程序中如何访问C语言程序中的全局变量。如果你想使用其他程序加载C程序例如汇编语言写的程序这部分很有用特别是在核心开发时经常用到。 int myVal0x5; int main() { }
编译这段代码 gcc –c test.c ld –Ttext 0x0 –e main –N –oformat binary –Map memmap.txt –o test.bin test.o objdump –D –b binrary –m i386 test.bin
得到如下结果 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: 05 .byte 0x5 15: 00 00 add %al,(%eax)
全局变量myVal存储在0x14。刚才已经使用-Map开关使ld生成了内存映像文件memmap.txt应该能够找到 .data 0x00000014 0x4 *(.data .data.* .gnu.linkonce.d.*) .data 0x00000014 0x4 test.o 0x00000014 myVal
说明myVal位于test.o模块的0x00000014位置。使用地址作为偏移量就可以直接在其他语言中访问myVal变量了。另为也可以通过memmap.txt查到BSS段的大小 cat memmap.txt grep ‘/.bss’ grep ‘0x’ sed ‘s/.*0x/0x/’
本例子BSS的大小是0x0。
无法直接访问C程序中的使用static修饰的全局变量。因为这样的变量是静态的map文件中没有列出他们的地址。也许你可以使用其他办法做到但是最好不要这样做。
12. 生成其他格式的二进制文件的选项
生成不同格式的二进制文件是一件相当麻烦的事。它需要使用很多不常使用的选项并且有些在man的帮助信息中没有被列出。
首先是gcc的选项-nostdinc。很显然使用该选项后gcc就不搜索默认的include路径了通常是/usr/include。如果需要使用的自定义的头文件可以使用-I选项添加搜索路径。
然后是ld的选项。第一个是-nostdlib就是忽略标准库。如果需要可以使用-L选项指定库的搜索路径。第二个是-Ttext就是指定代码段的地址如果没有继续指定其他段的地址则他们将自动的一次被放在代码段之后。第三个是-e就是指定代码的入口地址默认的是_start如果代码不是以其开头就应该指定入口点。第四个是—oformat binary就是输出的文件是原始的二进制文件而是如文件可以使系统支持的任何文件。但是中间模块文件不能是原始的二进制文件因为还需要很多符号和重定位信息。可以使用—iformat选项指定输入文件的格式但通常很少使用。第五个是-static如果使用了其他库用该使用静态链接方式除非你的程序支持动态链接。
另外还有代码指示伪指令。汇编器可以编译16-bit代码也可以编译32-bit代码。但是gcc总是生成32-bit的汇编代码。通过在C代码中使用asm()伪指令可以让gcc生成16-bit汇编代码。
第一个是.code16即生成在16-bit段中运行的16-bit代码
第二个是.code32即生成在32-bit段中运行的32-bit代码默认情况下gcc总是这么做
第三个是.code16gccgcc将根据需要决定生成在16-bit段下运行的16-bit或32-bit代码。GAS将会加上必要的前缀指示32-bit的指令或寄存器等。这个选项是很有用的它允许我们使用C语言写在16-bit环境下运行的代码不论是实模式还是保护模式。
现在可以在一个C模块中既有16-bit代码又有32-bit代码但是此时需要注意不同部分代码的地址空间问题。
例如我们想使用gcc生成在DOS下运行的.com程序和启动引导程序。
首先DOS中的.com文件是在实模式下运行的原始的二进制文件其起始地址为0x100。要使用gcc生成.com文件在每一个.c文件的开头加上如下伪指令 __asm__(“code16gcc/n”);
如果需要引用其他库文件则这些库文件也需要按这种方式生成。在链接时加上如下选项 -Ttext 0x100 –static –oformat binary
如果程序中包含嵌入的汇编代码需要将其转换为ATT格式。
如果要写引导程序只需要在链接时使用0x7C00代替0x100另外最终生成的二进制代码必须小于446个字节
13. 参考资料
l Intel Architecture Software Developer’s Manual
l Manual Pages in Linux
l Redhat GNUPro Toolkit