- 第一章 CPU 简介
- 第二章 Hello,world!
- 第三章 函数开始和结束
- 第四章 栈
- Chapter 5 printf() 与参数处理
- Chapter 6 scanf()
- CHAPER7 访问传递参数
- Chapter 8 一个或者多个字的返回值
- Chapter 9 指针
- Chapter 10 条件跳转
- 第 11 章 选择结构 switch()/case/default
- 第 12 章 循环结构
- 第 13 章 strlen()
- Chapter 14 Division by 9
- chapter 15 用 FPU 工作
- Chapter 16 数组
- Chapter 17 位域
- 第 18 章 结构体
- 19 章 联合体
- 第二十章 函数指针
- 第 21 章 在 32 位环境中的 64 位值
- 第二十二章 SIMD
- 23 章 64 位化
- 24 章 使用 x64 下的 SIMD 来处理浮点数
- 25 章 温度转换
- 26 章 C99 的限制
- 27 章 内联函数
- 第 28 章 得到不正确反汇编结果
- 第 29 章 花指令
- 第 30 章 16 位 Windows
- 第 31 章 类
- 三十二 ostream
- 34.2.2 MSVC
- 34.2.3 C++ 11 std::forward_list
- 34.3 std::vector
- 34.4 std::map and std::set
2.1 x86
2.1.1 MSVC-x86
在 MSVC 2010 中编译一下:
#!bash
cl 1.cpp /Fa1.asm
(/Fa 选项表示生产汇编列表文件)
#!bash
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。
所以例子可以改写为:
#!cpp
#include <stdio.h>
const char *$SG3830="hello, world";
int main() {
printf($SG3830);
return 0;
};
我们回到汇编列表,正如我们看到的,字符串是由 0 字节结束的,这也是 C/C++的标准。
在代码部分, _TEXT
,只有一个函数:main()。
函数 main() 与大多数函数一样都有开始的代码与结束的代码。
函数当中的开始代码结束以后,调用了 printf() 函数: CALL _printf
。
在 PUSH 指令的帮助下,我们问候语字符串的地址(或指向它的指针)在被调用之前存放在栈当中。
当 printf() 函数执行完返回到 main() 函数的时候,字符串地址(或指向它的指针) 仍然在堆栈中。
当我们都不再需要它的时候,堆栈指针(ESP 寄存器)需要改变。
#!bash
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
,因为这比 ADD ESP, X
需要的字节数更短,(1 字节对应 3 字节)。
在调用 printf() 之后,在 C/C++代码之后执行 return 0
,return 0 是 main() 函数的返回结果。
代码被编译成指令 XOR EAX, EAX
XOR 事实上就是异或,但是编译器经常用它来代替 MOV EAX, 0
原因就是它需要的字节更短(2 字节对应 5 字节)。
有些编译器用 SUB EAX, EAX
就是 EXA 的值减去 EAX,也就是返回 0。
最后的指令 RET 返回给调用者,他是 C/C++代码吧控制返还给操作系统。
2.1.2 GCC-x86
现在我们尝试同样的 C/C++代码在 linux 中的 GCC 4.4.1 编译
#!bash
gcc 1.c -o 1
下一步,在 IDA 反汇编的帮助下,我们看看 main() 函数是如何被创建的。
(IDA,与 MSVC 一样,也是显示 Intel 语法)。
我也可以是 GCC 生成 Intel 语法的汇编代码,添加参数
#!bash
-S -masm=intel
汇编代码:
#!bash
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”字符串地址(保存在 data 段的)一开始保存在 EAX 寄存器当中,然后保存到栈当中。
同样的在函数开始我们看到了
AND ESP, 0FFFFFFF0h
这条指令该指令对齐在 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...)。
2.1.3 GCC:AT & T 语法
我们来看一看在 AT&T 当中的汇编语法,这个语法在 UNIX 当中更普遍。
#!bash
gcc -S 1_1.c
我们将得到这个:
#!bash
.file "1_1.c"
.section .rodata
.LC0:
.string "hello, world"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $16, %esp
movl $.LC0, (%esp)
call printf
movl $0, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu/Linaro 4.7.3-1ubuntu1) 4.7.3"
.section .note.GNU-stack,"",@progbits
有很多的宏(用点开始)。现在为了简单起见,我们先不看这些。(除了 .string ,就像一个 C 字符串编码一个 null 结尾的字符序列)。然后,我们将看到这个:
#!bash
.LC0:
.string "hello, world"
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $16, %esp
movl $.LC0, (%esp)
call printf
movl $0, %eax
leave
ret
在 Intel 与 AT&T 语法当中比较重要的区别就是:
操作数写在后面
在 Intel 语法中:<instruction> <destination operand> <source operand>
在 AT&T 语法中:<instruction> <source operand> <destination operand>
有一个理解它们的方法: 当你面对 intel 语法的时候,你可以想象把等号放到 2 个操作数中间,当面对 AT&T 语法的时候,你可以放一个右箭头(→)到两个操作数之间。
AT&T: 在寄存器名之前需要写一个百分号(%) 并且在数字前面需要美元符($)。方括号被圆括号替代。 AT&T: 一些用来表示数据形式的特殊的符号
l long(32 bits)
w word(16bits)
b byte(8 bits)
让我们回到上面的编译结果:它和在 IDA 里看到的是一样的。只有一点不同:0FFFFFFF0h 被写成了 $-16
,但这是一样的,10 进制的 16 在 16 进制里表示为 0x10。-0x10 就等同于 0xFFFFFFF0
(这是针对于 32 位构架)。
外加返回值这里用的 MOV 来设定为 0,而不是用 XOR。MOV 仅仅是加载(load)了变量到寄存器。指令的名称并不直观。在其他的构架上,这条指令会被称作例如”load”这样的。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论