本篇使用gnu as作为测试工具,使用AT&T语法,在Unix界,AT&T语法占据绝对的地位。但是在Windows,Intel语法占据绝对的地位。
8086和80286是16位CPU,80386是32位CPU,所以现在的32位X86架构可以说是从80386开始的,386的寄存器相对于前两款CPU有了扩展,寄存器前的E表示Extend。
i386有8个32位通用寄存器,如下图:
约定,esp作为栈指针 (stack pointer),ebp作为基指针 (base pointer)。分别指向栈顶和栈底。对于eax, ebx, ecx和edx,子区域也可以使用。如eax的低16位,可以当作寄存器使用,叫ax,低16位也可以分成2个8位寄存器使用,叫ah和al。
共有6个段寄存器
CS
DS
SS
ES
FS
GS
由于现代操作系统使用平坦内存模式和分页机制,Linux系统很少使用段寄存器。
EFLAGS寄存器存储处理器的状态,如比较指令的结果,一些指令利用这些状态,如条件跳转指令。
EIP寄存器存储下一条要执行的指令地址。取值时CS指令也是计算依据。通常不应该直接访问该寄存器,而是使用跳转、调用指令等。
使用 gcc -S 可是将源文件编译为汇编文件。默认生成AT&T语法的汇编文件,使用-masm=intel,可以生成intel格式的汇编文件。在学习汇编代码之前,可以从c文件生成一个汇编文件,逆向学习汇编语法格式。如下是a.c文件
变量的定义格式如下。
1 2 3 4 5 6 7 | .globl var .data .align 4 .type var, @object .size var, 4 var: .long 10 |
应该放在.data之后,使用“.类型 值”的形式定义,基本类型有byte、short、long 3种,分别占用1/2/4个字节。然后在之前加上标签。标签不是必须的,标签是当前数据的地址,在其它地方可以使用标签。还有其它定义形式:
1 2 3 | .long 1,2,3 .zero 10 .string "hello" |
第一行,类似指定一个数组。第二行,10个字节的数据,全部为0。第三行,一个字符串。当然,需要.globl声明这个符号,以及指定对齐、类型、大小等。
和变量定义类似
1 2 3 4 5 | .text .globl func .type func, @function func: 汇编指令 |
应该放在.text之后,先声明符号和类型,然后给出标签,标签之后是函数的汇编指令。
指令由操作码和操作数组成。
有3类操作数,立即数、寄存器和存储器引用。下面是3种操作数的AT&T语法:
立即数,使用$加一个数字,如$1, $-20, $0x12等。
寄存器,使用%加上寄存器的名字,如%ebp, %ah等。
存储器引用,格式为Imm(Ea,Ei,s),存储器的值为Imm + Ea + Ei * s,s必须为1,2,4,8。可以省略Imm, Ei和s。如下合法的存储器引用:(%ebp), -1(%ebp), 7(%edx,%edx,4)。甚至可以直接是数字表示的内存地址,如0x100.
如果标签var是变量,则var表示的变量的内存地址,$var表示的内存地址的立即数。
mov var, %eax,把var地址处内存的值拷贝到eax。
mov $var, %eax,把var地址拷贝到寄存器eax。
mov var+1, %eax,把var+1地址处内存的值拷贝到eax。
mov $var+1, %eax,把var+1地址拷贝到寄存器eax。
把标签想象成一个表示地址的数即可,如0x100。3.1中数可以出现的地方,标签也可以出现,如var, $var, var(,1)等。
操作指令需要制定操作数据的长度,使用操作后缀的形式,后缀有
b,单字节,如movb。
w,双字节,如movw。
l,四字节,如movl。
包括mov指令,栈操作指令等。
用于在寄存器之间、寄存器和内存之间拷贝数据。mov指令有:
1 2 3 4 5 | mov reg, reg mov imm, reg mov mem, reg mov reg, mem mov imm, mem |
其中
reg格式是固定的:%eax等。
mem格式有2种,寄存器存储的地址形式: (%eax),其二是直接地址:0x100。
立即数格式只有一种:$2等。
mov可不指定后缀(默认为l),或指定操作后缀,如movl等。
用于把操作数放入栈中,语法
1 2 3 | push reg push mem push imm |
push操作先把%ebp减4,然后把数据存储到%ebp指向的内存。push可不指定后缀(默认为l),或者只能指定后缀l。
用于把数据弹出栈,并放入操作数中,语法
1 2 | pop reg pop mem |
pop先把%ebp指向的内存的数据拷贝到操作数中,然后把%ebp加4,pop可不指定后缀(默认为l),或者只能指定后缀l。
全称load effective address,把内存地址加载到寄存器,而不是内存地址处的数据,语法
1 | lea mem, reg |
如,lea var, %eax或者lea (%eax, %eax, 2), %ebx。lea通常用来计算指针的地址,或者执行简单的算术运算。lea可不指定操作后缀,或者指定后缀l,不支持后缀b和w。
包括加、减、与、或等。
将两个操作数相加,并把结果放入第二个操作数,语法
1 2 3 4 5 | add reg, reg add mem, reg add imm, reg add reg, mem add imm, mem |
可指定操作后缀,默认为l。
将第二操作数减去第一个操作数,结果存入第二个操作数,语法
1 2 3 4 5 | sub reg, reg sub mem, reg sub imm, reg sub reg, mem sub imm, mem |
可指定操作后缀,默认为l。
将寄存器或者内存数据+1,语法
1 2 | inc reg inc mem |
将寄存器或者内存数据-1,语法
1 2 | dec reg dec mem |
整数乘法(integer multiplication)指令,语法
1 2 3 4 5 | imul reg, reg imul mem, reg imul imm, reg imul imm, reg, reg imul imm, mem, reg |
有2个操作数和3个操作数两种格式。两个操作数的,将它们相乘,结果存入第二个操作数。三个操作数的,将第一个和第二个操作数相乘,结果存入第三个操作数。三个操作数的,第一个操作数必须是立即数。后缀默认是l,可以指定后缀为l或w。如果为w,操作数必须是16位寄存器。
整数除法(integer division)指令,语法
1 2 | idiv reg idiv mem |
被除数(dividend)放在EDX:EAX中,操作数是除数,商(quotient)存储在eax中, 余数(remainder)存储在edx中。
如果操作数是寄存器,则后缀必须和寄存器的位数匹配,如idivw %ax,如果寄存器是32位,l可以不指定。
如果操作数是内存,必须指定后缀。
逻辑与指令(Bitwise logical and),语法
1 2 3 4 5 | and reg, reg and mem, reg and imm, reg and reg, mem and imm, mem |
将两个操作数相与,结果存入第二个操作数。
逻辑或指令(Bitwise logical or),语法同and指令。
逻辑异或指令(Bitwise logical exclusive or),语法同and指令。
逻辑非指令(Bitwise logical not),即按位取反,语法
1 2 | not reg not mem |
二进制补码取反(Two's complement negation)指令,就是取负数,语法
1 2 | neg reg neg mem |
左移位(shift left)指令,语法
1 2 3 4 | shl imm, reg shl imm, mem shl %cl, reg shl %cl, mem |
将第二个操作左移第一个操作数位,结果存入第二个操作数。位数只能是立即数或者特定的寄存器%cl。
右移位(shift right)指令,语法同shl指令。
x86使用EIP寄存器作为取指寄存器,EIP寄存器通常不能直接维护,而使用转移控制指令维护。
跳转(jump)指令,语法如下
1 | jmp imm |
立即数指定要跳转的地址,通常使用label。
条件跳转(conditional jump)指令,语法
1 2 3 4 5 6 7 | je imm # jump when equal jne imm # jump when not equal jz imm # jump when last result is zero jg imm # jump when greater than jge imm # jump when greater than or equal to jl imm # jump when less than jle imm # jump when less than or equal to |
条件存储在特定的寄存器中,通常先执行比较指令,再执行条件跳转指令。
比较(compare)指令,语法
1 2 3 4 5 | cmp reg, reg cmp mem, reg cmp imm, reg cmp reg, mem cmp imm, mem |
将操作数2减去操作数1,条件码(condition code)存储到机器状态字(machine status word)中,这条指令和sub的不同之处是不将减的结果存储到操作数2。可以指定后缀。
调用子函数指令,call将当前指令地址放入栈中,然后无条件跳转到操作数指定的地址去执行。
1 | call imm |
立即数通常是一个函数label。
ret指令实现子函数返回,通过将call保存在栈中的指令地址弹出实现。
用于恢复父函数的栈,在子函数返回前执行。等价于
1 2 | movl %ebp, %esp popl %ebp |
见4.2节。
调用约定主要涉及参数传递,和寄存器保存与恢复等。调用约定包括2部分:调用者规则和被调用者规则。
在调用函数之前,调用者应当:
将调用者保存的寄存器 (caller-saved register) 存储到栈中,这些寄存器包括EAX, ECX, EDX。由于这些寄存器被保存,所以子程序可以随意使用这些寄存器。
把参数以相反的顺序(最后一个参数首先入栈)压入栈中。
call指令,调用子函数。
在子函数返回后,调用者通过EAX寄存器找到返回值,为了恢复调用之前的状态,调用者应当:
移除栈上的参数
从栈上弹出调用者保存的寄存器
被调用者:
首先,应该通过下述汇编代码切换栈。
1 2 | pushl %ebp movl %esp, %ebp |
第一条指令将父函数的栈底压入栈,然后将父函数的栈顶寄存器拷贝到新的栈底寄存器。参数、返回地址、局部变量都在EBP寄存器执行的地址附近。
接着,分配局部变量地址,由于栈向下增长,通过将ESP寄存器减对应大小即可。
接着,将被调用者保存寄存器 (callee-saved register) 压入栈中,被调用者保存寄存器包括EBX, EDI, ESI。
上述3步之后,可以开始执行被调用者函数主体代码。在函数返回前,应当:
将返回值存储到EAX。
将栈顶重回到函数开始的地方。movl %ebp, %esp 即可完成该操作。
将存储到栈中的父函数的栈底恢复,popl %ebp 即可完成该操作。
使用ret指令返回父函数。
上述2,3点,使用一条汇编指令leave即可。
x86 registers. http://www.eecg.toronto.edu/~amza/www.mindsec.com/files/x86regs.html
x86 Assembly Guide. http://flint.cs.yale.edu/cs421/papers/x86-asm/asm.html