x86汇编基础

32bit的x86汇编基础,包括一些常规指令,内存和寻址模式

总觉得学了又好像没学······

寄存器

现代 x86 处理器有 8 个 32 bit 寄存器,如上图所示。

寄存器名字是早期计算机历史上流传下来的。

  • EAX:一般用作累加器(Accumulator)
  • EBX:一般用作基址寄存器(Base)
  • ECX:一般用来计数(Count)
  • EDX:一般用来存放数据(Data)
  • ESI:一般用作源变址(Source Index)
  • EDI:一般用作目标变址(Destinatin Index)
  • ESP:一般用作堆栈指针(Stack Pointer)
  • EBP:一般用作基址指针(Base Pointer)

现在大部分寄存器的名字已经失去了原来的意义,但有两个是例外:栈指针寄存器(Stack Pointer)ESP 和基址寄存器( Base Pointer)EBP

对于 EAX, EBX, ECX, EDX 四个寄存器,可以再将 32bit 划分成多个子寄存器, 每个子寄存器有专门的名字。例如 EAX 的高 16bit 叫 AX(去掉 E, 据说E表示 Extended),低 8bit 叫 AL (Low), 8-16bit 叫 AH (High)。

在汇编语言中,这些寄存器的名字是大小写无关的,既可以用 EAX,也可以写 eax

内存和寻址模式

程序重定位

  • 存放程序的为代码段,存放数据的为数据段
  • 真实的内存单元地址称为物理地址,而程序中的地址为逻辑地址

由于程序并不知道自己会被加载到哪,因此访存如果用绝对地址将会出错,在执行程序时就需要程序重定位这个操作。

该操作在汇编中通过org指令实现,如org 0A100h代表该程序中的所有标号都以0A100h做偏移。

内存分段

将内存分段后,程序只需要识别偏移地址就可以确定数据位置。程序重定位通过设置代码段CS寄存器和数据段DS寄存器实现。

在8086中,地址总线是20位的,需要将段寄存器左移4位(0x10h,相当于16进制左移1位)变为20位,然后再同偏移地址相加。

两种典型情况

  • 因为段寄存器是16位的,在段不重叠的情况下,最多可以将1MB的内存分成65536个段,每个段16B,偏移地址从0000H000FH
  • 同样在不允许段之间重叠的情况下,因为偏移地址也是16位,1MB的内存最多只能划分成16个段,每段长64KB,段地址由0000HF000H

声明静态数据区

.DATA:声明静态存储区

数据类型修饰语:

DB/db:Byte,1Byte

DW/dw:Word,2Byte

DD/dd:Double Word,4Bytes

🌰:

1
2
3
4
5
6
.DATA
var DB 64 ; 声明一个 byte 值, referred to as location var, containing the value 64.
var2 DB ? ; 声明一个未初始化 byte 值, referred to as location var2.
DB 10 ; 声明一个没有 label 的 byte 值, containing the value 10. Its location is var2 + 1.
X DW ? ; 声明一个 2-byte 未初始化值, referred to as location X.
Y DD 30000 ; 声明一个 4-byte 值, referred to as location Y, initialized to 30000.

在汇编中只有一维数组,没有二维和多维数组。一维数组其实就是内存中的一块连续区域。DUP 和字符串常量也是声明数组的两种方法。

🌰:

1
2
3
4
Z       DD 1, 2, 3      ; 声明 3 个 4-byte values, 初始化为 1, 2, and 3. The value of location Z + 8 will be 3.
bytes DB 10 DUP(?) ; 声明 10 个 uninitialized bytes starting at location bytes.
arr DD 100 DUP(0) ; 声明 100 个 4-byte words starting at location arr, all initialized to 0
str DB 'hello',0 ; 声明 6 bytes starting at the address str, 初始化为 hello and the null (0) byte.

内存寻址

有多个指令可以用于内存寻址。如果要访问某一大小的内存,则通过添加修饰词byteworddword实现。

🌰:

其中MOV 将在内存和寄存器之间移动数据,接受两个参数:第一个参数是目的地,第二个是源。

1
mov byte[ebx], 2

函数调用(call)堆栈组织

Caller规则

  1. 在调用函数/子程序(subroutine)之前,先保存特定寄存器的状态(caller-saved)(包括eaxecxedx
  2. 将要传的参数堆栈(注意要逆序,最后一个参数最先入)。因为栈往下生长,因此第一个参数会被存在最低的地址
  3. 调用函数,call会将返回地址eip压入栈中
  4. 返回时先把参数移出栈,然后将原来保存的寄存器再pop出来

Callee规则

  1. ebp推入栈,将esp的值拷贝入ebp

    1
    2
    push ebp
    mov ebp, esp
  2. 分配局部变量,栈由上向下增长,如分配3个4B,则sub esp, 12

  3. 保存寄存器状态

常见指令

机器指令通常分为三类:数据移动、算术/逻辑和控制流。

接下来的符号解释如下:

1
2
3
4
5
6
7
8
9
<reg32> ; 任何32位寄存器 (EAX, EBX, ECX, EDX, ESI, EDI, ESP, or EBP)
<reg16> ; 任何16位寄存器 (AX, BX, CX, or DX)
<reg8> ; 任何8位寄存器 (AH, BH, CH, DH, AL, BL, CL, or DL)
<reg> ; 任何寄存器
<mem> ; 一个内存地址 (e.g., [eax], [var + 4], or dword ptr [eax+ebx])
<con32> ; 任何32位常量
<con16> ; 任何16位常量
<con8> ; 任何8位常量
<con> ; 任何8、16、32位常量

数据移动

mov

mov指令将第二操作对象(寄存器、内存内容或是常量值)所引用的数据项复制到其第一操作对象(寄存器或是内存)所引用的位置。

寄存器到寄存器的移动是合法的,但是直接内存到内存的移动是不合法的。在需要内存传输的情况下,必须首先将源内存中的内容加载到寄存器中,然后才能将其存储到目标内存地址。

语法:

1
2
3
4
5
mov <reg>,<reg>
mov <reg>,<mem>
mov <mem>,<reg>
mov <reg>,<const>
mov <mem>,<const>

🌰:

1
2
mov eax, ebx ; 将EBX中的值复制到EAX
mov byte ptr [var], 5 ; 将5存储到地址var的一个字节中

push

ESP(堆栈指针)通过push递减。

push指令将其操作对象放在内存中硬件支持堆栈的顶部。具体地说,PUSH首先将ESP递减4,然后将其操作对象放入内存地址[ESP]处的32位大小的区域中。

语法:

1
2
3
push <reg32>
push <mem>
push <con32>

🌰:

1
2
push eax ; 将eax入栈
push [var] ; 将地址var处开始的4个字节入栈

pop

pop指令将4字节数据元素从硬件支持的堆栈顶部移至指定的操作对象(即寄存器或内存位置)。它首先将位于内存位置[SP]的4个字节移动到指定的寄存器或内存位置,然后将SP递增4。

语法:

1
2
pop <reg32>
pop <mem>

🌰:

1
2
pop edi ; 将堆栈的顶部元素弹出到EDI中
pop [ebx] ; 将堆栈的顶部元素弹出到内存从EBX位置开始的四个字节中

lea

lea指令将其第二个操作对象指定的地址放入其第一个操作对象指定的寄存器中。需要注意的是,内存位置的内容不会被加载,并且只有有效地址会被计算并放入寄存器中。这对于获取指向内存区域的指针非常有用。

语法:

1
lea <reg32>,<mem>

🌰:

1
2
lea edi, [ebx+4*esi] ; 将地址EBX+4*ESI放入EDI
lea eax, [var] ; 将var中的值放在EAX中

算数和逻辑运算符

add-整数加法

add指令将其两个操作对象相加,将结果存储在第一个操作对象中。需要注意的是,虽然两个操作对象都可以是寄存器,但最多只有一个操作对象可以是内存位置

语法:

1
2
3
4
5
add <reg>,<reg>
add <reg>,<mem>
add <mem>,<reg>
add <reg>,<con>
add <mem>,<con>

🌰:

1
2
add eax, 10 ; EAX ← EAX + 10
add BYTE PTR [var], 10 ; 将存储在内存地址var的单字节值加上10

sub-整数减法

sub指令将其第一个操作对象的值减去第二个对象的值,并将结果存储在第一个对象的内存位置。

语法:

1
2
3
4
5
sub <reg>,<reg>
sub <reg>,<mem>
sub <mem>,<reg>
sub <reg>,<con>
sub <mem>,<con>

🌰:

1
2
sub al, ah ; AL ← AL - AH
sub eax, 216 ; 从存储在EAX中的值中减去216

inc,dec-递增,递减

inc指令将其操作对象的内容+1;DEC指令将其操作对象的内容-1

语法:

1
2
3
4
inc <reg>
inc <mem>
dec <reg>
dec <mem>

🌰:

1
2
dec eax ; 从EAX的内容中减去1
inc DWORD PTR [var] ; 将存储在位置var的32位整数加1

imul-整数乘法

imul指令有两种基本格式:两个操作对象和三个操作对象。

有两个操作对象时将其两个操作对象相乘,并将结果储存在第一个操作对象当中,其中,第一个对象必须是寄存器。

有三个操作对象时,将第二个操作对象与第三个操作对象相乘,并将其结果储存在第一个操作对象当中,其中第一个对象必须是寄存器,第三个对象必须是常量值。

语法:

1
2
3
4
imul <reg32>,<reg32>
imul <reg32>,<mem>
imul <reg32>,<reg32>,<con>
imul <reg32>,<mem>,<con>

🌰:

1
2
imul eax, [var] ; 将EAX的内容乘以内存位置var的32位内容并将结果存储在EAX中
imul esi, edi, 25 ; ESI → EDI * 25

idiv-整数除法

idiv指令将64位整数EDX:EAX的内容除以指定的操作对象值。结果存储在EAX中,其余数的存储在EDX中。

语法:

1
2
idiv <reg32>
idiv <mem>

🌰:

1
2
idiv ebx ; 将EDX:EAX的内容除以EBX的内容。把商放在EAX中,余放在EDX中
idiv DWORD PTR [var] ; 将EDX:EAX的内容除以存储在内存位置var的32位值。把商放在EAX中,余放在EDX中

and,or xor-按位与、或和异或

这些指令对其操作对象执行指定的位运算(分别为按位与、或和异或),并将结果放在第一个操作对象位置。

语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
and <reg>,<reg>
and <reg>,<mem>
and <mem>,<reg>
and <reg>,<con>
and <mem>,<con>

or <reg>,<reg>
or <reg>,<mem>
or <mem>,<reg>
or <reg>,<con>
or <mem>,<con>

xor <reg>,<reg>
xor <reg>,<mem>
xor <mem>,<reg>
xor <reg>,<con>
xor <mem>,<con>

🌰:

1
2
and eax, 0fH ; 清除EAX的除最后4位以外的所有位
xor edx, edx ; 将EDX的内容设置为零

not-按位取反

not指令触发反转操作对象中的所有位,其结果称为反码。

语法:

1
2
not <reg>
not <mem>

🌰:

1
not BYTE PTR [var] ; 取反内存位置var的字节中的所有位

neg-求补

neg是汇编指令中的求补指令,对操作对象执行求补运算:用0减去操作对象,然后结果返回操作对象;或是直接将操作对象按位取反后+1

语法:

1
2
neg <reg>
neg <mem>

🌰:

1
neg eax ; EAX → - EAX

shl, shr-左移,右移

这些指令将其第一个操作对象内容中的位左右移位,用零填充产生的空位位置。移位后的操作对象最多可以移位31位。要移位的位数由第二个操作对象指定,该操作对象可以是8位常量,也可以是寄存器CL。

在任一情况下,以32为模执行大于31的移位计数。

语法:

1
2
3
4
5
6
7
8
9
shl <reg>,<con8>
shl <mem>,<con8>
shl <reg>,<cl>
shl <mem>,<cl>

shr <reg>,<con8>
shr <mem>,<con8>
shr <reg>,<cl>
shr <mem>,<cl>

🌰:

1
2
shl eax, 1 ; 将EAX的值乘以2(如果最高有效位为0)
shr ebx, cl ; 将EBX的值除以2^n^的结果的下限存储在EBX中,其中n是CL中的值

控制流指令

x86处理器维护一个指令指针(IP)寄存器,它是一个32位值,指示当前指令在内存中的起始位置。通常,在执行一条指令后,它会递增以指向内存中的下一条指令的起始位置。IP寄存器不能直接操作,而是由提供的控制流指令隐式更新。

我们使用符号<LABEL>来表示代码中已标记的位置。通过输入标签名称后跟冒号,可以在x86汇编代码中的任意位置插入标签。

例如:

1
2
3
       mov esi, [ebp+8]
begin: xor ecx, ecx
mov eax, [esi]

此代码段中的第二条指令被标记为begin。在代码的其他地方,我们可以使用更方便的符号名称begin来引用此指令所在的内存中的位置。这个标签只是表示位置的一种方便方式,而不是它的32位值。

jmp-跳转

将程序控制流转移到操作对象指示的内存位置上

语法:

1
jmp <label>

🌰:

1
jmp begin ; 跳到标记为begin的指令位置

jcondition-条件跳转

这些指令是基于一组条件码状态判断是否进行跳转,该条件码被存储在称为机器状态字的特殊寄存器中。

机器状态字的内容包括有关上次执行的算术运算的信息。例如,此字的某一比特位表示最后结果是否为零,某另一个比特位指示上次结果是否为负数。

基于这些条件码,可以执行多个条件跳转。例如,如果上次算术运算的结果为零,则JZ指令执行到指定操作对象标签的跳转。否则,控制按顺序前进到下一条指令。

许多条件分支的名字都是根据上一次执行的特殊比较指令cmp命名的。例如,条件分支(如JLE和JNE)基于首先对所需操作对象执行cmp操作。

语法:

1
2
3
4
5
6
7
je <label>  ; 相等时跳转
jne <label> ; 不相等时跳转
jz <label> ; 最后结果为零时跳转
jg <label> ; 大于时跳转
jge <label> ; 大于等于时跳转
jl <label> ; 小于时跳转
jle <label> ; 小于等于时跳转

🌰:

1
2
cmp eax, ebx
jle done ; 如果EAX的中的值小于或等于EBX中的值,跳至标签done。否则,继续执行下一条指令

cmp-比较

比较两个指定操作对象的值,适当设置机器状态字中的条件代码。此指令等同于sub指令,不同之处在于将丢弃减法结果,而不是替换第一个操作对象。

语法:

1
2
3
4
cmp <reg>,<reg>
cmp <reg>,<mem>
cmp <mem>,<reg>
cmp <reg>,<con>

🌰:

1
2
cmp DWORD PTR [var], 10
jeq loop ; 如果存储在var中的4个字节的值等于4字节整数常量10,则跳转到标记为loop的位置

call, ret-子程序调用和返回

这些指令实现一个子程序调用和返回。

call指令首先将当前代码位置压入到内存中硬件支持的堆栈中,然后无条件跳转到标签操作对象指示的代码位置。与简单的跳转指令不同,call指令保存当前位置,并在子程序完成时返回到此处。

ret指令实现子程序返回机制。此指令首先从硬件支持的内存堆栈中弹出代码位置,然后无条件跳转至该代码位置。

语法:

1
2
call <label>
ret

最近一直被问x86学完了没,这算学完叭(小声bb)🤨


x86汇编基础
https://shmodifier.github.io/2023/03/27/x86汇编基础/
作者
Modifier
发布于
2023年3月27日
许可协议