本文主要关注在Windows平台下使用gcc进行AT&T汇编语法的学习,通过gcc对C代码进行反汇编学习,而不是手动编写汇编代码,目的是能读懂《深入理解计算机系统》,未完…
MinGW
MinGW是一个在Windows平台上的GNU开发工具包。
下载地址为 http://sourceforge.net/projects/mingw/files/ 。
Cygwin
在Windows平台下模拟Linux开发环境最好的方法是安装Cygwin。Cygwin官网上对自己的解释是,一个在Windows上的一套Linux开发工具和环境,并包含一个提供Linux API的DLL(cygwin1.dll)。
下载地址为 http://cygwin.com/install.html 。
寄存器
学习汇编语言之首先要了解下寄存器的概念。
CPU最慢的操作是读取内存中的数据或者把数据存储到内存中。当处理器访问数据元素时,请求必须被发送到处理器外部、通过控制总线,然后进入内存存储单元。
为了解决上述问题,在处理器内部固化了一些存储部件,称为寄存器。寄存器能够存储要处理的数据元素,而无需访问内存。
IA-32平台下的主要寄存器有一下几类:
通用寄存器
段寄存器
指令寄存器1. 通用寄存器是8个32为寄存器,用于存储正在处理的数据
EAX 用于操作数和结果的累加器
EBX 指向数据内存段中的数据的指针
ECX 字符串和循环操作的计数器
EDX IO指针
EDI 用于字符串操作的目标数据指针
ESI 用于字符串操作的源数据指针
ESP 堆栈指针
EBP 堆栈数据指针
前4个通用寄存器可也可以通过16位或者8位名称引用,以表示寄存器的老版本。例如:EAX的低16位成为AX,AX的高8位称为AH,低8位称为AL。2. 段寄存器, 6个16位寄存器,用于处理内存访问。
CS 代码段
DS 数据段
SS 堆栈段
ES 附加段指针
FS 附加段指针
GS 附加段指针
每个段寄存器都是16位的,包含指向内存特定段的起始位置的指针。CS包含指向内存代码段中的指针。代码段是内存中存储指令码的位置。处理器按照CS寄存器的值和EIP指令指针寄存器中包含的偏移值从内存获得指令码。程序不能显式地加载或者改变CS寄存器。当程序被分配一个内存空间时,处理器将为CS寄存器赋值。3. 指令指针寄存器,EIP有时也被称为程序计数器,它指向要执行的下一条指令。
4. 标志寄存器,一个16位的标志寄存器,通常称为程序状态字(Program Status Word,PSW)PSW。
CF:进位标志,最高位产生了进位或借位,CF=1,否则CF=0
ZF:零标志,预算结果为0,ZF=1,否则ZF=0
SF:符号标志,运算结果为负,SF=1,否则SF=0
OF:溢出标志,有符号数的运算如果发生了溢出,OF=1,否则OF=0
AT&T汇编语法格式不同与Intel语法格式,主要区别如下:
AT&T使用$表示立即操作数,而Intel的立即操作数不需要有界定的。因此,使用AT&T语法引用十进制4时,使用$4,使用Intel语法时只需要4。
AT&T在寄存器前面加上前缀%,而Intel不这样做。因此使用AT&T语法引用EAX寄存器时写为%eax。
AT&T语法处理源和目标操作数时使用相反的顺序。把十进制4传送给EAX,AT&T的语法是movl $4, %eax,而Inter语法是mov eax, 4。
AT&T语法在助记符后面使用一个单独的字符来引用操作数中使用的数据长度,而Intel语法中数据长度被声明为单独的操作数。AT&T的指令movl $test, %eax等同于Intel语法的mov eax, dword ptr test。
长调用和跳转使用不同语法定义段和偏移值。AT&T语法使用ljmp $section, $offset,而Intel语法使用jmp section:offset。
使用gcc把C语言编译成汇编语言
C语言的源代码如下(hello.c)
#include < stdio.h > int main() { printf("hello, world\n"); }
AT&T格式的汇编代码是GCC的默认格式,使用命令 gcc –S hello.c 可以把hello.c编译成AT&T格式的汇编代码。
.file " hello.c " .def ___main ; .scl 2; .type 32; .endef .section .rdata, " dr " LC0: .ascii " hello, world\0 " .text .globl _main .def _main ; .scl 2; .type 32; .endef _main: LFB6: .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 call ___main movl $LC0, (%esp) call _puts leave .cfi_restore 5 .cfi_def_cfa 4 , 4 ret .cfi_endproc LFE6: .def _puts ; .scl 2; .type 32; .endef
所有以’.’开头的行都是指导汇编器和连接器的命令。
使用命令 gcc –S –masm=intel helloc可以生成Intel格式的汇编代码。
.file " hello.c " .intel_syntax noprefix .def ___main ; .scl 2; .type 32; .endef .section .rdata, " dr " LC0: .ascii " hello, world\0 " .text .globl _main .def _main ; .scl 2; .type 32; .endef _main: LFB6: .cfi_startproc push ebp .cfi_def_cfa_offset 8 .cfi_offset 5 , - 8 mov ebp, esp .cfi_def_cfa_register 5 and esp, - 16 sub esp, 16 call ___main mov DWORD PTR [esp], OFFSET FLAT: LC0 call _puts leave .cfi_restore 5 .cfi_def_cfa 4 , 4 ret .cfi_endproc LFE6: .def _puts ; .scl 2; .type 32; .endef
操作数
大多数指令有一个或多个操作数,指示出执行一个操作中的源数据值,以及放置结果的目标位置。有三种不同类型的操作数:
1. 立即数。以$ + 整数方式表示,例如:%4、%0x80。
2. 寄存器。可以是32位、16位或者8位的寄存器,例如%eax、%ax或%al。我们用Ea来表示寄存器a,用引用R[Ea]来表示它的值,这将寄存器看成一个数组R,用寄存器标识符作为索引。
3. 存储器引用。他会根据计算出来的地址访问某个存储器位置。因为将存储器看成一个很大的字节数组,我们用符号M[Addr]表示存储器中从地址Addr开始的字节值的引用。
Imm[Eb, Ei, s]是最常用的形式,它有四部分组成。一个立即数翩翩Imm,一个基址寄存器Eb,一个变址寄存器Ei和一个比例因子s,有效地址被计算为Imm + R[Eb] + R[Ei] * s
数据传送指令
最基本的传送指令是MOV。
MOV S, D 表示将S操作数中的值复制到D操作数中。下面是它的变种:
movb 传送字节
movw 传送字
movl 传送双字
movsbw 将做了符号扩展的字节传送到字
movsbl 将做了符号扩展的字节传送到双字
movswl 将做了符号扩展的字传送到双字
movzbw 将做了零扩展的字节传送到字
movzbl 将做了零扩展的字节传送到双字
movzwl 将做了零扩展的字传送到双字
出栈入栈指令
pushl S 将双字压栈
popl S 将双字出栈
指令pushl %ebp 等价于下面两条指令
subl $4, %esp
movl %ebp, (%esp)指令popl %eax 等价于下面两条指令
movl (%esp), %eax
addl $4, %esp
算数与逻辑操作指令
gdb
使用gcc编译源代码时必须加入 –g 参数,才可以使用gdb进行调试。
b main命令表示在main函数的开始出设置断点,r表示运行程序