1 编译器模式(1)

当我第一次学会了C和C++时候,我只是编写短的代码块,然后编译它,查看它在汇编语言中是什么样子。

这对我来说很容易,因为我这样做了很多次。C /C++代码和编译器产生的代码的对应关系印在我的脑海中如此之深,以至于当我看了x86代码的代码使便可以很快还原它的C代码。

也许这项技术对于别人来说是有用的,所以我将尝试描述一些例子。

1.1 Hello, world!
让我们从《C语言程序设计》书中著名的例子开始

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

1.1.1 x86

MSVC

我们在MSVC 2010中编译它

cl 1.cpp /Fa 1.asm

(/Fa选项意味着生成汇编代码)

Listing 1.1: MSVC 2010

CONST SEGMENT
$SG3830 DB ’hello, world’, 00H
CONST ENDS
PUBLIC _main
EXTRN _printf:PROC
; Function compile flags: /Odtp
_TEXT SEGMENT
_main PROC
      push ebp
      mov ebp, esp
      push OFFSET $SG3830
      call _printf
      add esp, 4
      xor eax, eax
      pop ebp
      ret 0
_main ENDP
_TEXT ENDS

MSVC生成的汇编代码使用Intel语法。Intel语法和AT&T语法的区别将在后续章节讨论。

编译器生成的1.obj文件将被链接为1. exe。

在我们的例子中,该文件包含两个段:CONST(数据常量)和_TEXT(代码)。

字符串“hello,world”在C /C++是const char *类型,但是它没有自己的名字。编译器需要使用该字符串,因此定义了一个内部名称$SG3830。

正如我们看到的,这是个标准的C / C++字符串,字符串是由00作为结束标志。

在代码段,_TEXT,只有一个函数:main()。函数main()由prologue code开始,由epilogue code结束(像几乎所有函数那样)

在我们看到调用了printf()函数:CALL _printf

在调用字符串地址(或者是指针)之前它在指令PUSH的帮助下压入堆栈。当printf()函数返回流控制给main()时,字符串地址(或者是指针)仍然保存在堆栈中。

当我们不再需要它时只需要修正栈指针(ESP寄存器)即可。ADD ESP, 4 意思是让ESP寄存器的值加4。为何是4呢?因为它是32位的代码,我们需要4字节地址出栈。

64位代码的话要加8。“ADD ESP, 4”相当于“POP register”, 但是该命令不需使用任何寄存器。

一些编译器(如Intel c++编译器)在相同的情况下可能使用POP ECX而不是用ADD。(如我们可以在Oracle RDBMS中看到,因为它使用Intel c++编译器)。这条指令

可以达到相同的作用但是ECX寄存器被改写了。

Intel C++ 编译器可能会使用POP ECX是因为它的操作码(Opcode)比ADD ESP, 4要更短。(1字节相对于3字节)

可以在1.2小结阅读更多的堆栈的知识。

调用printf()之后,最初的C/C++代码将会返回0-返回0作为main()函数的结果。在生成的代码中,由指令XOR EAX,EAX实现。XOR实际上是“异或”但是编译器通常用它

代替MOV EAX, 0-用样的道理是因为操作码比较短(2字节相对于5字节)。一些编译器可能会使用SUB EAX, EAX,它的含义是从eax的值中减去eax的值,结果也是0。

最后一个指令RET向调用者返回控制流。通常C/C++运行库(CRT,C runtime library)会将控制权交还给操作系统。

 

 GCC

让我们在linux的GCC 4.4.1中编译相同的C/C++代码:gcc 1.c -o 1

然后在IDA的帮助下我们看看main()函数是怎么生成的。(IDA同MSVC一样使用INTEL语法)

注意 我们也可以用GCC生成intel语言的汇编代码,只需要通过添加-S -masm=intel选项

Listing 1.2: GCC

main proc near
var_10 = dword ptr -10h
       push ebp
     mov ebp, esp
    and esp, 0FFFFFFF0h
    sub esp, 10h
    mov eax, offset aHelloWorld ; "hello, world"
    mov [esp+10h+var_10], eax
    call _printf
    mov eax, 0
    leave
    retn
main endp

结果几乎完全一样。“hello world” 字符串的地址首先存在EAX寄存器中然后入栈。函数的prologue中我们可以看到AND ESP, 0FFFFFFF0h使得ESP的值按16字节边界对齐。

结果使得栈中所有的值对齐。(如果内存定位是按照4或者16字节对齐,CPU能够更好的工作)SUB ESP, 10h在栈中分配了16字节空间,尽管在下面我们看到只需要4个字节。

原因在于在栈中也是16字节对齐的。然后字符串地址(或指针)没有使用PUSH指令直接写入栈空间。var_10 —一个局部变量同时也是printf()的参数。然后printf()函数被调用。

和msvc不同的是,GCC编译器在不选择优化的时候,使用MOV EAX, 0而不是用比较短的操作码。最后一个指令,leave-相当于MOV ESP, EBP和POP EBP指令的组合-换句话说,

这个指令使得栈指针返回并且恢复EBP寄存器的初始状态。这是必要的,因为我们在函数的开始修改了这些寄存器(ESP和EBP)的值(执行了MOV EBP, ESP / AND ESP, ...)

GCC: AT&T 语法

让我们看看这些在的汇编语言的AT&T语法如何表达。这个语法在UNIX世界更受欢迎。

Listing 1.3: let’s compile in GCC 4.7.3

gcc -S 1_1.c

我们可以得到这些:

Listing 1.4: GCC 4.7.3

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

 


里面有很多的宏(以点开头)。目前我们对它不感兴趣。现在为了简化我们可以忽略它们。(除了以null结尾的字符编码的.string宏,就像C字符串一样)

然后得到如下代码:

Listing 1.5: GCC 4.7.3

 1 .LC0:
 2     .string "hello, world"
 3 main:
 4     pushl %ebp
 5     movl %esp, %ebp
 6     andl $-16, %esp
 7     subl $16, %esp
 8     movl $.LC0, (%esp)
 9     call printf
10     movl $0, %eax
11     leave
12     ret

其中Intel and AT&T语法的一些区别是:

∙操作数位置相反:

Intel语法: <指令> <目的操作数> <源操作数>.

AT&T语法: <指令> <源操作数> <目的操作数>.

可以这样考虑:使用Intel语法时你可以在脑海中在操作数之间加个(=)等号,而使用AT&T语法时你可以加个向右的箭头(→)

AT&T:寄存器名字前必须加个百分号(%),立即数前面必须加个美元符号($),使用圆括号代替方括号。

AT&T:在每条指令后添加一个特殊的符号来定义数据类型

– l — 长字 (32 bits)
– w — 字 (16 bits)
– b —字节 (8 bits)

让我们回到编译结果:

它和我们在IDA里看到的结果是相同的,只有一个很小的区别:0FFFFFFF0h写成了$-16。它们是一样的:十进制的16在十六进制中是0x10。0x10等于0xfffffff0(32位数据类型)。

另外,使用MOV将返回值设为0,而不是XOR。MOV仅将值加载到寄存器,它的名字不太合适(数据没有移动),这条指令在其他架构下叫“load”或者其他类似的名字。

 

 

 

posted @ 2013-12-25 20:27  莫名の悲伤  阅读(533)  评论(0)    收藏  举报