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,偏移地址从
0000H
到000FH
- 同样在不允许段之间重叠的情况下,因为偏移地址也是16位,1MB的内存最多只能划分成16个段,每段长64KB,段地址由
0000H
到F000H
声明静态数据区
.DATA
:声明静态存储区
数据类型修饰语:
DB/db
:Byte,1Byte
DW/dw
:Word,2Byte
DD/dd
:Double Word,4Bytes
🌰:
1 |
|
在汇编中只有一维数组,没有二维和多维数组。一维数组其实就是内存中的一块连续区域。DUP
和字符串常量也是声明数组的两种方法。
🌰:
1 |
|
内存寻址
有多个指令可以用于内存寻址。如果要访问某一大小的内存,则通过添加修饰词byte
、word
、dword
实现。
🌰:
其中MOV
将在内存和寄存器之间移动数据,接受两个参数:第一个参数是目的地,第二个是源。
1 |
|
函数调用(call)堆栈组织
Caller规则
- 在调用函数/子程序(subroutine)之前,先保存特定寄存器的状态(caller-saved)(包括
eax
、ecx
、edx
) - 将要传的参数堆栈(注意要逆序,最后一个参数最先入)。因为栈往下生长,因此第一个参数会被存在最低的地址
- 调用函数,
call
会将返回地址eip
压入栈中 - 返回时先把参数移出栈,然后将原来保存的寄存器再pop出来
Callee规则
将
ebp
推入栈,将esp
的值拷贝入ebp
1
2push ebp
mov ebp, esp分配局部变量,栈由上向下增长,如分配3个4B,则
sub esp, 12
保存寄存器状态
常见指令
机器指令通常分为三类:数据移动、算术/逻辑和控制流。
接下来的符号解释如下:
1 |
|
数据移动
mov
mov指令将第二操作对象(寄存器、内存内容或是常量值)所引用的数据项复制到其第一操作对象(寄存器或是内存)所引用的位置。
寄存器到寄存器的移动是合法的,但是直接内存到内存的移动是不合法的。在需要内存传输的情况下,必须首先将源内存中的内容加载到寄存器中,然后才能将其存储到目标内存地址。
语法:
1 |
|
🌰:
1 |
|
push
ESP(堆栈指针)通过push递减。
push指令将其操作对象放在内存中硬件支持堆栈的顶部。具体地说,PUSH首先将ESP递减4,然后将其操作对象放入内存地址[ESP]处的32位大小的区域中。
语法:
1 |
|
🌰:
1 |
|
pop
pop指令将4字节数据元素从硬件支持的堆栈顶部移至指定的操作对象(即寄存器或内存位置)。它首先将位于内存位置[SP]的4个字节移动到指定的寄存器或内存位置,然后将SP递增4。
语法:
1 |
|
🌰:
1 |
|
lea
lea指令将其第二个操作对象指定的地址放入其第一个操作对象指定的寄存器中。需要注意的是,内存位置的内容不会被加载,并且只有有效地址会被计算并放入寄存器中。这对于获取指向内存区域的指针非常有用。
语法:
1 |
|
🌰:
1 |
|
算数和逻辑运算符
add-整数加法
add指令将其两个操作对象相加,将结果存储在第一个操作对象中。需要注意的是,虽然两个操作对象都可以是寄存器,但最多只有一个操作对象可以是内存位置。
语法:
1 |
|
🌰:
1 |
|
sub-整数减法
sub指令将其第一个操作对象的值减去第二个对象的值,并将结果存储在第一个对象的内存位置。
语法:
1 |
|
🌰:
1 |
|
inc,dec-递增,递减
inc指令将其操作对象的内容+1;DEC指令将其操作对象的内容-1
语法:
1 |
|
🌰:
1 |
|
imul-整数乘法
imul指令有两种基本格式:两个操作对象和三个操作对象。
有两个操作对象时将其两个操作对象相乘,并将结果储存在第一个操作对象当中,其中,第一个对象必须是寄存器。
有三个操作对象时,将第二个操作对象与第三个操作对象相乘,并将其结果储存在第一个操作对象当中,其中第一个对象必须是寄存器,第三个对象必须是常量值。
语法:
1 |
|
🌰:
1 |
|
idiv-整数除法
idiv指令将64位整数EDX:EAX
的内容除以指定的操作对象值。结果存储在EAX中,其余数的存储在EDX中。
语法:
1 |
|
🌰:
1 |
|
and,or xor-按位与、或和异或
这些指令对其操作对象执行指定的位运算(分别为按位与、或和异或),并将结果放在第一个操作对象位置。
语法:
1 |
|
🌰:
1 |
|
not-按位取反
not指令触发反转操作对象中的所有位,其结果称为反码。
语法:
1 |
|
🌰:
1 |
|
neg-求补
neg是汇编指令中的求补指令,对操作对象执行求补运算:用0减去操作对象,然后结果返回操作对象;或是直接将操作对象按位取反后+1
语法:
1 |
|
🌰:
1 |
|
shl, shr-左移,右移
这些指令将其第一个操作对象内容中的位左右移位,用零填充产生的空位位置。移位后的操作对象最多可以移位31位。要移位的位数由第二个操作对象指定,该操作对象可以是8位常量,也可以是寄存器CL。
在任一情况下,以32为模执行大于31的移位计数。
语法:
1 |
|
🌰:
1 |
|
控制流指令
x86处理器维护一个指令指针(IP)寄存器,它是一个32位值,指示当前指令在内存中的起始位置。通常,在执行一条指令后,它会递增以指向内存中的下一条指令的起始位置。IP寄存器不能直接操作,而是由提供的控制流指令隐式更新。
我们使用符号<LABEL>来表示代码中已标记的位置。通过输入标签名称后跟冒号,可以在x86汇编代码中的任意位置插入标签。
例如:
1 |
|
此代码段中的第二条指令被标记为begin。在代码的其他地方,我们可以使用更方便的符号名称begin来引用此指令所在的内存中的位置。这个标签只是表示位置的一种方便方式,而不是它的32位值。
jmp-跳转
将程序控制流转移到操作对象指示的内存位置上
语法:
1 |
|
🌰:
1 |
|
jcondition-条件跳转
这些指令是基于一组条件码状态判断是否进行跳转,该条件码被存储在称为机器状态字的特殊寄存器中。
机器状态字的内容包括有关上次执行的算术运算的信息。例如,此字的某一比特位表示最后结果是否为零,某另一个比特位指示上次结果是否为负数。
基于这些条件码,可以执行多个条件跳转。例如,如果上次算术运算的结果为零,则JZ指令执行到指定操作对象标签的跳转。否则,控制按顺序前进到下一条指令。
许多条件分支的名字都是根据上一次执行的特殊比较指令cmp命名的。例如,条件分支(如JLE和JNE)基于首先对所需操作对象执行cmp操作。
语法:
1 |
|
🌰:
1 |
|
cmp-比较
比较两个指定操作对象的值,适当设置机器状态字中的条件代码。此指令等同于sub指令,不同之处在于将丢弃减法结果,而不是替换第一个操作对象。
语法:
1 |
|
🌰:
1 |
|
call, ret-子程序调用和返回
这些指令实现一个子程序调用和返回。
call指令首先将当前代码位置压入到内存中硬件支持的堆栈中,然后无条件跳转到标签操作对象指示的代码位置。与简单的跳转指令不同,call指令保存当前位置,并在子程序完成时返回到此处。
ret指令实现子程序返回机制。此指令首先从硬件支持的内存堆栈中弹出代码位置,然后无条件跳转至该代码位置。
语法:
1 |
|
最近一直被问x86学完了没,这算学完叭(小声bb)🤨