2.1 x86

    (/Fa 选项表示生产汇编列表文件)

    MSVC生成的是Intel汇编语法。Intel语法与AT&T语法的区别将在后面讨论。

    编译器会把1.obj文件连接成1.exe。

    在我们的例子当中,文件包含两个部分:CONST(放数据)和_TEXT(放代码)。

    字符串"hello,world"在C/C++ 类型为const char*,然而它已经丢失了自己的名称。

    编译器需要处理这个字符串,就自己给他定义了一个$SG3830。

    所以例子可以改写为:

    1. #include <stdio.h>
    2. const char *$SG3830="hello, world";
    3. int main() {
    4. printf($SG3830);
    5. return 0;
    6. };

    我们回到汇编列表,正如我们看到的,字符串是由0字节结束的,这也是C/C++的标准。

    在代码部分,_TEXT,只有一个函数:main()。

    函数main()与大多数函数一样都有开始的代码与结束的代码。

    函数当中的开始代码结束以后,调用了printf()函数:CALL _printf

    在PUSH指令的帮助下,我们问候语字符串的地址(或指向它的指针)在被调用之前存放在栈当中。

    当printf()函数执行完返回到main()函数的时候,字符串地址(或指向它的指针)仍然在堆栈中。

    当我们都不再需要它的时候,堆栈指针(ESP寄存器)需要改变。

    ADD ESP, 4

    意思是ESP寄存器加4。

    在64位系统当中它是8字节。

    ADD ESP, 4实际上等同于POP register

    一些编辑器(如Intel C++编译器)在同样的情况下可能会用POP ECX代替ADD(例如这样的模式可以在Oracle RDBMS代码中看到,因为它是由Intel C++编译器编译的),这条指令的效果基本相同,但是ECX的寄存器内容会被改写。

    Intel C++编译器可能用POP ECX,因为这比ADD ESP, X需要的字节数更短,(1字节对应3字节)。

    在调用printf()之后,在C/C++代码之后执行return 0return 0是main()函数的返回结果。

    代码被编译成指令XOR EAX, EAX

    XOR事实上就是异或,但是编译器经常用它来代替原因就是它需要的字节更短(2字节对应5字节)。

    有些编译器用SUB EAX, EAX 就是EXA的值减去EAX,也就是返回0。

    最后的指令RET 返回给调用者,他是C/C++代码吧控制返还给操作系统。

    现在我们尝试同样的C/C++代码在linux中的GCC 4.4.1编译

    gcc 1.c -o 1

    下一步,在IDA反汇编的帮助下,我们看看main()函数是如何被创建的。

    (IDA与MSVC一样,也是显示Intel语法)。

    我也可以是GCC生成Intel语法的汇编代码,添加参数-S -masm=intel

    汇编代码:

    结果几乎是相同的,"hello,world"字符串地址(保存在data段的)一开始保存在EAX寄存器当中,然后保存到栈当中。

    同样的在函数开始我们看到了

    这条指令以16字节边界大小对齐ESP寄存器,这使得所有值的地址在栈上都会有相同的对齐方式。

    SUB ESP,10H在栈上分配16个字节。 这里其实只需要4个字节。

    这是因为,分配堆栈的大小也被排列在一个16字节的边界。

    该字符串的地址(或这个字符串指针),不使用PUSH指令,直接写入到堆栈空间。var_10,是一个局部变量,也是printf()的参数。

    然后调用printf()函数。

    不像MSVC,当gcc编译不开启优化,它使用MOV EAX,0清空EAX,而不是更短的代码。

    最后一条指令,LEAVE相当于MOV ESP,EBP和POP EBP两条指令。

    换句话说,这相当于指令将堆栈指针(ESP)恢复,EBP寄存器到其初始状态。

    这是必须的,因为我们在函数的开头修改了这些寄存器的值(ESP和EBP)(执行MOV EBP,ESP/AND ESP…)。

    我们来看一看在AT&T当中的汇编语法,这个语法在UNIX当中更普遍。

    gcc -S 1_1.c

    我们将得到这个:

    1. .file "1_1.c"
    2. .section .rodata
    3. .LC0:
    4. .text
    5. .globl main
    6. .type main, @function
    7. main:
    8. .LFB0:
    9. .cfi_startproc
    10. pushl %ebp
    11. .cfi_offset 5, -8
    12. movl %esp, %ebp
    13. .cfi_def_cfa_register 5
    14. andl $-16, %esp
    15. movl $.LC0, (%esp)
    16. call printf
    17. movl $0, %eax
    18. leave
    19. .cfi_restore 5
    20. .cfi_def_cfa 4, 4
    21. ret
    22. .cfi_endproc
    23. .LFE0:
    24. .size main, .-main
    25. .ident "GCC: (Ubuntu/Linaro 4.7.3-1ubuntu1) 4.7.3"
    26. .section .note.GNU-stack, "", @progbits

    有很多的宏(用点开始)。现在为了简单起见,我们先不看这些。(除了 .string ,就像一个C字符串编码一个null结尾的字符序列)。然后,我们将看到这个:

    在Intel与AT&T语法当中比较重要的区别就是:

    操作数写在后面

    1. AT&T语法中:<instruction> <source operand> <destination operand>

    有一个理解它们的方法: 当你面对intel语法的时候,你可以想象把等号放到2个操作数中间,当面对AT&T语法的时候,你可以放一个右箭头(→)到两个操作数之间。

    AT&T: 在寄存器名之前需要写一个百分号(%)并且在数字前面需要美元符($)。方括号被圆括号替代。 AT&T: 一些用来表示数据形式的特殊的符号

    外加返回值这里用的MOV来设定为0,而不是用XOR。MOV仅仅是加载(load)了变量到寄存器。指令的名称并不直观。在其他的构架上,这条指令会被称作例如”load”这样的。