ILD

i386汇编指令初学
作者:Herbert Yuan 邮箱:yuanjp89@163.com
发布时间:2017-6-28 站点:Inside Linux Development

本篇使用gnu as作为测试工具,使用AT&T语法,在Unix界,AT&T语法占据绝对的地位。但是在Windows,Intel语法占据绝对的地位。

1 寄存器 Registers

8086和80286是16位CPU,80386是32位CPU,所以现在的32位X86架构可以说是从80386开始的,386的寄存器相对于前两款CPU有了扩展,寄存器前的E表示Extend。

1.1 通用寄存器 General purpose registers

i386有8个32位通用寄存器,如下图:

约定,esp作为栈指针 (stack pointer),ebp作为基指针 (base pointer)。分别指向栈顶和栈底。对于eax, ebx, ecx和edx,子区域也可以使用。如eax的低16位,可以当作寄存器使用,叫ax,低16位也可以分成2个8位寄存器使用,叫ah和al。

1.2 段寄存器 Segment registers

共有6个段寄存器

由于现代操作系统使用平坦内存模式和分页机制,Linux系统很少使用段寄存器。

1.3 EFLAGS寄存器

EFLAGS寄存器存储处理器的状态,如比较指令的结果,一些指令利用这些状态,如条件跳转指令。

1.4 指令指针寄存器 Instruction Pointer Register

EIP寄存器存储下一条要执行的指令地址。取值时CS指令也是计算依据。通常不应该直接访问该寄存器,而是使用跳转、调用指令等。

2 AT&T汇编代码

使用 gcc -S 可是将源文件编译为汇编文件。默认生成AT&T语法的汇编文件,使用-masm=intel,可以生成intel格式的汇编文件。在学习汇编代码之前,可以从c文件生成一个汇编文件,逆向学习汇编语法格式。如下是a.c文件

2.1 定义变量

变量的定义格式如下。

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声明这个符号,以及指定对齐、类型、大小等。

2.2 定义函数

和变量定义类似

1
2
3
4
5
    .text
    .globl  func
    .type   func, @function
func:
    汇编指令

应该放在.text之后,先声明符号和类型,然后给出标签,标签之后是函数的汇编指令。

3 指令 Instructions

指令由操作码和操作数组成。

3.1 操作数

有3类操作数,立即数、寄存器和存储器引用。下面是3种操作数的AT&T语法:

3.2 标签作操作数

如果标签var是变量,则var表示的变量的内存地址,$var表示的内存地址的立即数。

把标签想象成一个表示地址的数即可,如0x100。3.1中数可以出现的地方,标签也可以出现,如var, $var, var(,1)等。

3.3 操作后缀 Operation suffixes

操作指令需要制定操作数据的长度,使用操作后缀的形式,后缀有

3.4 数据传送指令 Data movement instructions

包括mov指令,栈操作指令等。

3.4.1 mov指令

用于在寄存器之间、寄存器和内存之间拷贝数据。mov指令有:

1
2
3
4
5
mov reg, reg
mov imm, reg
mov mem, reg
mov reg, mem
mov imm, mem

其中

mov可不指定后缀(默认为l),或指定操作后缀,如movl等。

3.4.2 push指令

用于把操作数放入栈中,语法

1
2
3
push reg
push mem
push imm

push操作先把%ebp减4,然后把数据存储到%ebp指向的内存。push可不指定后缀(默认为l),或者只能指定后缀l。

3.4.3 pop指令

用于把数据弹出栈,并放入操作数中,语法

1
2
pop reg
pop mem

pop先把%ebp指向的内存的数据拷贝到操作数中,然后把%ebp加4,pop可不指定后缀(默认为l),或者只能指定后缀l。

3.4.4 lea指令

全称load effective address,把内存地址加载到寄存器,而不是内存地址处的数据,语法

1
lea mem, reg

如,lea var, %eax或者lea (%eax, %eax, 2), %ebx。lea通常用来计算指针的地址,或者执行简单的算术运算。lea可不指定操作后缀,或者指定后缀l,不支持后缀b和w。

3.5 算术与逻辑指令 Arithmetic and logic instructions

包括加、减、与、或等。

3.5.1 add指令

将两个操作数相加,并把结果放入第二个操作数,语法

1
2
3
4
5
add reg, reg
add mem, reg
add imm, reg
add reg, mem
add imm, mem

可指定操作后缀,默认为l。

3.5.2 sub指令

将第二操作数减去第一个操作数,结果存入第二个操作数,语法

1
2
3
4
5
sub reg, reg
sub mem, reg
sub imm, reg
sub reg, mem
sub imm, mem

可指定操作后缀,默认为l。

3.5.3 inc指令

将寄存器或者内存数据+1,语法

1
2
inc reg
inc mem

3.5.4 dec指令

将寄存器或者内存数据-1,语法

1
2
dec reg
dec mem

3.5.5  imul指令

整数乘法(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位寄存器。

3.5.6 idiv指令

整数除法(integer division)指令,语法

1
2
idiv reg
idiv mem

被除数(dividend)放在EDX:EAX中,操作数是除数,商(quotient)存储在eax中,  余数(remainder)存储在edx中。

如果操作数是寄存器,则后缀必须和寄存器的位数匹配,如idivw %ax,如果寄存器是32位,l可以不指定。

如果操作数是内存,必须指定后缀。

3.5.7 and指令

逻辑与指令(Bitwise logical and),语法

1
2
3
4
5
and reg, reg
and mem, reg
and imm, reg
and reg, mem
and imm, mem

将两个操作数相与,结果存入第二个操作数。

3.5.8 or指令

逻辑或指令(Bitwise logical or),语法同and指令。

3.5.9 xor指令

逻辑异或指令(Bitwise logical exclusive or),语法同and指令。

3.5.10 not指令

逻辑非指令(Bitwise logical not),即按位取反,语法

1
2
not reg
not mem

3.5.11 neg指令

二进制补码取反(Two's complement negation)指令,就是取负数,语法

1
2
neg reg
neg mem

3.5.12 shl指令

左移位(shift left)指令,语法

1
2
3
4
shl imm, reg
shl imm, mem
shl %cl, reg
shl %cl, mem

将第二个操作左移第一个操作数位,结果存入第二个操作数。位数只能是立即数或者特定的寄存器%cl。

3.5.13 shr指令

右移位(shift right)指令,语法同shl指令。

3.6 控制转义指令 Control flow instructions

x86使用EIP寄存器作为取指寄存器,EIP寄存器通常不能直接维护,而使用转移控制指令维护。

3.6.1 jmp指令

跳转(jump)指令,语法如下

1
jmp imm

立即数指定要跳转的地址,通常使用label。

3.6.2 jcondition指令

条件跳转(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

条件存储在特定的寄存器中,通常先执行比较指令,再执行条件跳转指令。

3.6.3 cmp指令

比较(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。可以指定后缀。

3.6.4 call指令

调用子函数指令,call将当前指令地址放入栈中,然后无条件跳转到操作数指定的地址去执行。

1
call imm

立即数通常是一个函数label。

3.6.5 ret指令

ret指令实现子函数返回,通过将call保存在栈中的指令地址弹出实现。

3.6.6 leave指令

用于恢复父函数的栈,在子函数返回前执行。等价于

1
2
movl %ebp, %esp
popl %ebp

见4.2节。

4 调用约定 Calling convention

调用约定主要涉及参数传递,和寄存器保存与恢复等。调用约定包括2部分:调用者规则和被调用者规则。

4.1 调用者规则 Caller rules

在调用函数之前,调用者应当:

  1. 将调用者保存的寄存器 (caller-saved register) 存储到栈中,这些寄存器包括EAX, ECX, EDX。由于这些寄存器被保存,所以子程序可以随意使用这些寄存器。

  2. 把参数以相反的顺序(最后一个参数首先入栈)压入栈中。

  3. call指令,调用子函数。

在子函数返回后,调用者通过EAX寄存器找到返回值,为了恢复调用之前的状态,调用者应当:

  1. 移除栈上的参数

  2. 从栈上弹出调用者保存的寄存器

4.2 被调用者规则 Callee rules

被调用者:

1
2
pushl %ebp
movl %esp, %ebp

第一条指令将父函数的栈底压入栈,然后将父函数的栈顶寄存器拷贝到新的栈底寄存器。参数、返回地址、局部变量都在EBP寄存器执行的地址附近。

上述3步之后,可以开始执行被调用者函数主体代码。在函数返回前,应当:

  1. 将返回值存储到EAX。

  2. 将栈顶重回到函数开始的地方。movl %ebp, %esp 即可完成该操作。

  3. 将存储到栈中的父函数的栈底恢复,popl %ebp 即可完成该操作。

  4. 使用ret指令返回父函数。

上述2,3点,使用一条汇编指令leave即可。

References

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


Copyright © linuxdev.cc 2017-2024. Some Rights Reserved.