还是在大学的时候学汇编原理和接口技术的时候看过这些东西,不甚明了,现在就来重新看一下。

前言

粗略的来看,CPU 是一个计算单元,但其要计算的指令是从存储器,RAM来的。然后一般来说,一个程序在运行的时候,会将硬盘中的代码以一定的形式解析后加载到 RAM 中,事实上,CPU 的工作做成,就是从 RAM 拿数据,拿指令,然后执行的过程。

所谓的寻址,就是如何定位位于 RAM 中的内存单元的问题了。

寄存器

X86 当前已经定义了很多的寄存器了。大体来说分为四类:

  • 通用寄存器 EAX, EBX, ECX, EDX。
  • 段寄存器 CS, DS, ES, GS, SS
  • 索引和指针寄存器 ESI, EDI, EBP, EIP, ESP
  • 标识寄存器 EFLAGS

通用寄存器

这个是用得最多的寄存器了,但是他们还可以分开来,当做 8/16 位的寄存器来使用:

32 bits : EAX EBX ECX EDX
16 bits : AX BX CX DX
8 bits : AH AL BH BL CH CL DH DL

在这里 H,L 指的是寄存器的高字节和低字节:

EAX,AX,AH,AL : 加法寄存器(累加器) 
用于 I/O 端口访问,算数运算,中断调用等等。

EBX,BX,BH,BL : 基址寄存器。用来作为一个指向内存位置的指针。(在分段模式下,其指向的是 DS 寄存器内的)。
还可用来获取一些中断的返回值。


ECX,CX,CH,CL : 计数寄存器。
用以循环计数或者是位移。
获取一些中断的返回值。

EDX,DX,DH,DL : 数据寄存器
I/O端口访问,算数运算,一些中断调用

段寄存器

段寄存器保存的是在分段模式下各数据段,代码段的地址。他们是16位的值。,他们只能被通用寄存器设置值,或特殊的指令设置值。

CS         : 持有程序的代码段。
改变它,可能会导致程序挂起。

DS : 程序的数据段。
改变它可能会导致获取不正确的数据。

ES,FS,GS : 扩展寄存器,用来针对远距离指针寻址。

SS : 程序使用的堆栈段寄存器。有些时候和 DS 一样。
改变它的址会有不可预知的结果,其与很多数据相关。

索引和指针寄存器

索引和指针寄存器指定地址的偏移部分。用途很广,但是每个寄存器都有一个特殊的功能。有的时候用来和一个段寄存器配合,以指定一个很远的地址。 “E” 开头的寄存器只能在保护模式下使用。

ES:EDI EDI DI : 目的索引寄存器。
用来做字符,内存数组的复制或者设置及配合 ES 进行远距离寻址。

DS:ESI EDI SI : 源索引寄存器。
用来做字符,内存数组的复制或者设置

SS:EBP EBP BP : 堆栈基址寄存器。
持有堆栈的基地址。

SS:ESP ESP SP : 堆栈寄存器。
持有堆栈顶地址。

CS:EIP EIP IP : 指令寄存器。只读。
下一条指令的偏移。

标志寄存器

Bit   Label    Desciption
---------------------------
0 CF Carry flag
2 PF Parity flag
4 AF Auxiliary carry flag
6 ZF Zero flag
7 SF Sign flag
8 TF Trap flag
9 IF Interrupt enable flag
10 DF Direction flag
11 OF Overflow flag
12-13 IOPL I/O Priviledge level
14 NT Nested task flag
16 RF Resume flag
17 VM Virtual 8086 mode flag
18 AC Alignment check flag (486+)
19 VIF Virutal interrupt flag
20 VIP Virtual interrupt pending flag
21 ID ID flag

寻址

间接内存寻址

间接寻址,指定了给定地址的内容。

比如,下面这个指令就是将 DS:BX 地址处,WORD 长单元内的内容给 AX

mov     ax, WORD PTR [bx]

其中:

  • WORD 指定数据的大小
  • PTR 将 [BX] 内存地址转换为 WORD 大小的值

我们可以指定多个寄存器:

mov ax, [ax+si]

地址偏移

地址偏移是将一个常量加到有效地址上。

table   WORD    100 DUP (0)
.
.
.
mov ax, table[ esi ]

在上面的例子中,表达式 table[esi] 表示 table 进行了偏移,table 在这里是提供了一个数组的地址。

ESI 是数组中一个元素的索引,一般是运行时在循环中进行计算出来。

多重地址偏移

每个偏移量都可以顺是一个地址或者数字的常量。汇编器会将所有的偏移量加起来。

table   WORD    100 DUP (0)
.
.
.
mov ax, table[bx][di]+6

table6 都是偏移量

操作数大小

必须对间接寻址指定地址大小。

  1. 通过变量指定大小
  2. 通过 PTR
  3. 通过其他操作数隐式指定

假设 table 的大小是 WORD

table   WORD    100 DUP (0)
.
.
.
mov table[bx], 0 ; 2 bytes - from size of table
mov BYTE PTR table, 0 ; 1 byte - specified by BYTE
mov ax, [bx] ; 2 bytes - implied by AX

间接语法选项

间接寻址有多种方式,

  • 但是所有的寄存器都必须在 [] 中,。
  • 可以在 [] 多个寄存器用 + 连接,或者 [] 每个寄存器再连接
mov     ax, table[bx][di]
mov ax, table[di][bx]
mov ax, table[bx+di]
mov ax, [table+bx+di]
mov ax, [bx][di]+table

上面几种方式其实都是一个效果,拿到 table 中 索引是 BX+DI 的元素到 AX

倍乘因子

主要是用来索引数组。例如

  • 1 表示 字节数组
  • 2 表示 word 数组
  • 4 表示 doubleword 数组
  • 8 表示 4字节数组

性能上不会有损失。

mov     eax, darray[edx*4]     ; Load double of double array
mov eax, [esi*8][edi] ; Load double of quad array
mov ax, wtbl[ecx+2][edx*2] ; Load word of word array

内存段与基础寄存器

在间接寻址中,基本寄存器 表示在哪个段内计算内存地址。所以我们需要知道哪个在间接寻址的时候是以哪个为寄存器为寄存进行寻址的。

  • 如果基础寄存器是,EBP, ESP ,那么针对 SS 段进行寻址。然而,如果 EBP 有倍乘因子,那么就是在 DS 内寻址。
  • 其他所有的基址寄存器都是在 DS 内。
  • 使用两个寄存器的时候,只能有一个有倍乘因子。(最多只能有两个寄存器进行寻址操作)
  • 有倍乘因子的那个寄存器,就是索引寄存器。
  • 其他的寄存器就是基址寄存器。
  • 如果没有倍乘因子,第一个就是基址寄存器。
mov   eax, [edx][ebp*4] ; EDX base (not scaled - seg DS)
mov eax, [edx*1][ebp] ; EBP base (not scaled - seg SS)
mov eax, [edx][ebp] ; EDX base (first - seg DS)
mov eax, [ebp][edx] ; EBP base (first - seg SS)
mov eax, [ebp] ; EBP base (only - seg SS)
mov eax, [ebp*2] ; EBP*2 index (seg DS)

总结

立即模式

不访问内存,操作数就是指令的一部分。

mov ax, 567
mov ah, 09h
mov dx, offset Prompt

寄存器寻址

不会访问内存,操作数在寄存器内

add ax, bx

间接模式

内存只访问一次,指令的操作数字段包含了操作的地址。

value dword 0
..
add ax, value ; 声明符号
add eax, [value]; 同上

汇编代码

tbl DW 20 DUP(0)
..
mov [tbl], 56

和 C 代码

tbl[0] = 50

一样。

寄存器间接寻址(间接寻址模式)

常用来在程序循环中寻址数组数据。

  • 操作数的有效地址在一个寄存器内
  • 32位寻址,所有32位寄存器都可用
  • 对于 16 位的寻址,偏移量可以在 BX,SI,DI 中。
mov bx, offset Table  ; Load address
add ax, [bx] ; Register indirect addressing
  • [BX] 表示 BX 内是一个地址偏移量。表现得像一个指针。

寄存器间接模式可以用来实现数组。比如我们要累加一个 word 数组:

       mov cx, size          ; set up size of Table
mov bx, offset Table ; BX <- address of Table
xor ax, ax ; zero out Sum
Loop1:
add ax, [bx]
inc bx ; each word is 2 bytes long, so
inc bx ; need to increment BX twice!
loop Loop1

索引

是一个常量基址+寄存器。

  • 固定地址(基址)+ 可变的寄存器偏移(操作数字段含有一个常量基址)
  • 有效地址是将操作数字段加到寄存器上。
  • 这也被叫做数组类型寻址,或者也叫做 偏移寻址。
mov eax, [ ebx + 5 ]
mov eax, [ ebx + esi + 5 ]

[] 内结合使用寄存器是有限制的:[] 不能同时出现 DI,SI,不能同时出现 EBX, EBP。

add ax, Table[ bx ]
add ax, [ Table + bx ]
add ax, Table + [ bx ]
add ax, [ bx ] + Table

这几种方式是相同的。

索引倍乘

基数+寄存器偏移*倍乘因子

栈寻址

PUSH,POP。 ESP 会自动的增加或者减少。

跳转相对地址

EIP+offset

直接跳转到一个地址。

例子

我们用一个 c 代码来看一下。

int add(int a, int b){
return a+b;
}
int main(){
int a = 2;
int b = 3;
return add(a,b);
}

编译成汇编代码:

c++  -S -mllvm --x86-asm-syntax=intel t.cpp

打开 t.s

	.section	__TEXT,__text,regular,pure_instructions
.build_version macos, 10, 14 sdk_version 10, 14
.intel_syntax noprefix
.globl __Z3addii ## -- Begin function _Z3addii
.p2align 4, 0x90
__Z3addii: ## @_Z3addii add 函数
.cfi_startproc
## %bb.0:
push rbp ;基址寄存器入栈
.cfi_def_cfa_offset 16
.cfi_offset rbp, -16
mov rbp, rsp ; 基址寄存器指向栈顶
.cfi_def_cfa_register rbp
mov dword ptr [rbp - 4], edi; 参数 a 的值复制到新的栈帧内存
mov dword ptr [rbp - 8], esi; 参数 b 的值复制到新的栈帧内存
mov esi, dword ptr [rbp - 4]; 将栈帧内 a 的值给到 esi
add esi, dword ptr [rbp - 8]; 加上参数 b 的值
mov eax, esi; 将结果给 eax
pop rbp; 恢复
ret
.cfi_endproc
## -- End function
.globl _main ## -- Begin function main
.p2align 4, 0x90
_main: ## @main 程序启动入口
.cfi_startproc
## %bb.0:
push rbp ;基址寄存器入栈
.cfi_def_cfa_offset 16
.cfi_offset rbp, -16
mov rbp, rsp; 基址寄存器指向栈顶
.cfi_def_cfa_register rbp
sub rsp, 16 ; 堆栈寄存器下移 16 字节。
mov dword ptr [rbp - 4], 0 ; 栈顶位置第一个4字节设置为0
mov dword ptr [rbp - 8], 2 ; 栈顶位置第二个4字节设置为2 int a = 2;
mov dword ptr [rbp - 12], 3; 栈顶位置第三个4字节设置为3 int b = 3;
mov edi, dword ptr [rbp - 8]; 参数 a 传入 DI
mov esi, dword ptr [rbp - 12]; 参数 b 传入 SI
call __Z3addii ; 调用 add 函数。
add rsp, 16 ; 栈指到栈顶
pop rbp ; BP 值向原来的值。
ret
.cfi_endproc
## -- End function

.subsections_via_symbols

通过上面例子就能看到,事实上调用函数的时候传的参数,过程是这样的:

  1. 将参数值复制到寄存器。
  2. 为函数开新的栈帧
  3. 寄存器中的值复制到栈上。
  4. 通过寄存器计算结果。
  5. 返回到主函数。

参考