X86汇编学习

一、基础知识

1.CPU

CPU控制整个计算机运作和运算。要想让CPU工作,就需要向它提供指令数据

2.指令

(1).汇编指令

直接被CPU执行的指令

(2).伪指令

指导汇编器工作的指令,不生成机械码。

  • 段定义指令:如SEGMENT、ENDS,用于定义和结束一个段。
  • 数据定义指令:如DB、DW,用于定义字节或字数据。
  • 常量定义指令:如EQU,用于定义常量。
(3).符号指令

用于标识变量、常量、代码段等,在汇编过程中被替换为具体的地址或值。它们没有对应的机器码。

  • 标签:用于标识代码中的位置。

    1
    LABEL: ; 定义一个标签LABEL
  • 宏定义:如MACRO,用于定义宏,简化代码编写。

    1
    MYMACRO MACRO ; 宏定义内容 ENDM

3.CPU对存储器的读写

  1. 存储单元的地址
  2. 控制信息
  3. 读或者写的数据

4.地址总线

CPU通过地址总线来指定存储器单元

一个CPU有N根地址线,则它的地址总线宽度为N,最多可以寻找2的N次方给内存单元。

CPU通过地址总线传入存储器的必须是一个内存单元的物理地址

5.数据总线

CPU与内存或其他器件之间数据传送是通过数据总线进行的。

6.控制总线

CPU对外部器件的控制是通过控制总线进行的。

7.内存地址空间

8086CPU地址总线宽度为20,可以定位2的20次方个内存单元,那么内存地址空间为1MB。

二、寄存器

(一)、通用寄存器

1.数据寄存器
(1)作为完整的32位数据寄存器
  • EAX,EBX,ECX,EDX
(2)下半部分作为16位寄存器
  • AX:累加器
  • BX:基址寄存器
  • CX:计数寄存器
  • DX:数据寄存器
(3)16位寄存器的下半部分和上半部分再作为8位寄存器
  • AH,AL,BLH,BL
  • CH,CL,DH,DL
2.指针寄存器
  • 指令指针(IP)
  • 堆栈指针(SP)
  • 基本指针(BP)
3.索引寄存器
  • 源索引(SI)
  • DI
4.数据寄存器

(二)、控制寄存器

将32位指令指针寄存器和32位标志寄存器组合。

通用标志位
  • 溢出标志(OF) -有符号算术运算后数据的高阶位(最左位)的溢出。
  • 方向标记(DF) -DF值为0时,字符串操作为从左至右的方向;当DF值为1时,字符串操作为从右至左的方向。
  • 中断标志(IF) -当值为0时,它禁用外部中断,而当值为1时,它使能中断。
  • 陷阱标志(TF) -允许在单步模式下设置处理器的操作。我们使用的DEBUG程序设置了陷阱标志,因此我们可以一次逐步执行一条指令。
  • 符号标志(SF) -正结果将SF的值清除为0,负结果将其设置为1。
  • 零标志(ZF) -非零结果将零标志清零,零结果将其清零。
  • 辅助进位标志(AF) -包含经过算术运算后从位3到位4的进位;用于专业算术。当1字节算术运算引起从第3位到第4位的进位时,将设置AF。
  • 奇偶校验标志(PF) -偶数个1位将奇偶校验标志清为0,奇数个1位将奇偶校验标志清为1。
  • 进位标志(CF) -在算术运算后,它包含一个高位(最左边)的0或1进位。

(三)、段寄存器

  • CS代码段寄存器,包含当前正在执行的代码的段基址。
  • DS数据段寄存器,通常包含程序正在操作的数据的段基址。
  • SS堆栈段寄存器,包含当前堆栈的段基址。
  • ES附加段寄存器,用于存储其他数据段的基址。
  • FS
  • GS

(四)8086CPU给出物理地址的方法

物理地址=段地址x16+偏移地址

用两个16位地址(段地址、偏移地址)合成一个20位物理地址

段地址 x 16 是十六进制段地址数据左移一位

(三)、内存和寻址

段分布

  • text: 存放的是二进制机器码,用于存储程序中已初始化的全局变量和静态变量,只读
  • .data: 用于存储程序中已初始化的全局变量和静态变量。非默认值
  • .bss:存放未初始化的全局变量,或者默认初始化的全局变量。这一部分在二进制文件中不占硬盘空间,即不会真实存储这些为初始化的变量,而是在程序加载到内存时再分配。当然肯定需要有个标识,告诉该怎么分配内存
  • .rodata:存放只读数据,如常量数据

声明静态数据区

.DATA声明静态数据区

在汇编中只有一维数组,只有没有二维和多维数组。

内存寻址

  • 立即寻址
1
mov rax, 123 ; rax = 123
  • 寄存器寻址
1
add rax, rbx ; rax = rax + rbx
  • 直接寻址
1
mov eax, [someVariable] ; eax = contents of memory at address someVariable
  • 间接寻址
1
mov eax, [rbx] ; eax = contents of memory at address stored in rbx
  • 基址寻址
1
mov eax, [rbx+4] ; eax = contents of memory at address (rbx + 4)
  • 索引寻址
1
mov eax, [rbx+rcx*4] ; eax = contents of memory at address (rbx + rcx*4)
  • 基址变址寻址
1
mov eax, [rbx+rcx+4] ; eax = contents of memory at address (rbx + rcx + 4)

Heap

因为用户主动请求而划分出来的内存区域,叫做 Heap(堆)。

它由起始地址开始,从低位(地址)向高位(地址)增长。

Heap 的一个重要特点就是不会自动消失,必须手动释放,或者由垃圾回收机制来回收。

Stack

除了 Heap 以外,其他的内存占用叫做 Stack(栈)。简单说,Stack 是由于函数运行而临时占用的内存区域。

img

(五)、指令

lea和mov指令

LEA指令将其第二个操作对象指定的地址放入其第一个操作对象指定的寄存器中。

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

1
2
3
4
5
mov eax, [ebx]	     ; 将EBX中的地址所指向的内存中的4个字节移动到EAX中
mov [var], ebx ; 将EBX的内容移到内存地址var的4个字节中(注意,不加中括号的var是一个32位地址常量,加中括号才是取地址指向的内容)
mov eax, [esi-4] ; 将内存地址ESI+(-4)上的4个字节移入EAX
mov [esi+eax], cl ; 将CL的内容移到地址为ESI+EAX的单字节中
mov edx, [esi+4*ebx] ; 将地址为ESI+4*EBX的4字节数据移动到EDX中

ADD、SUB指令

用于对字节,字和双字大小的二进制数据进行简单的加/减,即分别用于添加或减去8位,16位或32位操作数。

1
2
3
4
5
6
7
add <reg>,<reg>
add <reg>,<mem>
add <mem>,<reg>
add <reg>,<con>
add <mem>,<con>
sub al, ah ; AL ← AL - AH
sub eax, 216 ; 从存储在EAX中的值中减去216

INC和DEC指令

目标操作数可以是8位,16位或32位操作数。

INC指令用于将操作数加1。

DEC指令用于将操作数减1。

1
2
3
4
INC EBX      ;  32-bit 寄存器 自增1
INC DL ; 8-bit 寄存器 自增1
INC [count] ; 变量count 自增1

imul、idiv

整数乘除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
imul <reg32>,<reg32>
imul <reg32>,<mem>
imul <reg32>,<reg32>,<con>
imul <reg32>,<mem>,<con>

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

idiv <reg32>
idiv <mem>

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
and <reg>,<reg>
and <reg>,<mem>
and <mem>,<reg>
and <reg>,<con>
and <mem>,<con>

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

not、neg指令

NOT 指令触发(翻转)操作对象中的所有位。其结果被称为反码。

NEG是汇编指令中的求补指令,对操作对象执行求补运算:用零减去操作对象,然后结果返回操作对象。

(将操作对象按位取反后加1)

1
2
3
4
5
not <reg>
not <mem>

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

shl, shr指令

左移,右移

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

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

JMP及衍生

无条件跳转

JMP 无条件跳转指令
JC 进位时跳转
JO 溢出时跳转
JE/JZ 相等/零时跳转
JS 负数时跳转
JP/JPE 奇偶校验时跳转

CMP指令

比较两个操作数。

1
2
CMP DX, 00  ; 将DX值与0进行比较
JE L7 ; 如果等于,则跳转到标签L7

LOOP指令

实现循环(计数型循环)

CX = CX -1

判断CX中的值,不为0则继续跳转;为0则向下执行

1
2
LOOP    label

(六)、调用约定

系统调用

操作系统为用户态运行的进程与硬件设备之间进行交互提供了一组接口。

实质上就是\函数调用**,只不过调用的是\系统函数**,处于内核态而已

在Linux中,EAX寄存器是负责传递系统调用号的。

内联汇编

(一)、内联函数

内联汇编是指在高级语言中嵌入汇编代码,,减少函数调用。

(二)、内联汇编语法

1. 在 GCC 中

使用 asm__asm__ 关键字:

1
asm("assembly-code" : output : input : clobbered_registers);
1
2
3
4
5
int a = 10, b = 20, result;
asm("addl %%ebx, %%eax"
: "=a"(result) // 输出约束,`a` 表示使用 EAX 寄存器
: "a"(a), "b"(b) // 输入约束,`b` 表示使用 EBX 寄存器
: "cc"); // 通知编译器条件码被修改

关键约束:

  • 操作数约束
    • "r":任意寄存器
    • "m":内存操作数
    • "i":立即数
    • "a":EAX
    • "b":EBX 等
  • 修饰符
    • =::表示写入输出
    • +::表示读写操作
    • &::表示早期释放寄存器

2. 在 MSVC 中

使用 __asm 关键字:

1
2
3
4
__asm {
mov eax, 1
add eax, 2
}

示例:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main() {
int a = 5, b = 3, result;
__asm {
mov eax, a
add eax, b
mov result, eax
}
printf("Result: %d\n", result);
return 0;
}

(三)常用指令示例

1. 数据传送

1
2
mov eax, 10       ; 将 10 送入 EAX 寄存器
mov ebx, eax ; 将 EAX 的值传给 EBX

2. 算术运算

1
2
3
4
add eax, ebx      ; EAX = EAX + EBX
sub eax, 5 ; EAX = EAX - 5
mul ebx ; EAX = EAX * EBX
div ecx ; EAX = EAX / ECX

3. 条件跳转

1
2
3
cmp eax, ebx      ; 比较 EAX 和 EBX
je equal_label ; 如果相等,则跳转到 equal_label
jg greater_label ; 如果大于,则跳转到 greater_label

4. 栈操作

1
2
push eax          ; 将 EAX 压入栈
pop ebx ; 从栈中弹出值到 EBX