x86架构与汇编语言

本文简要说明x86架构及x86汇编语言的使用。

80x86的历史

1978年Intel发布第一款16位微处理器8086(也称为iAPX86),之后又发布了80186、80286、80386、80486,因而该系列的CPU架构被称为x86。

  • 8086是16位的处理器,80386(1985)是32位的
  • 8086采用16位数据总线,20位地址总线(相当于1MB内存);80386采用32位数据总线,32位地址总线
  • 8086只支持实模式,80386还支持保护模式

下面将介绍80386的一些基本汇编指令,注意这里采用nasm语法。

寄存器

现代的x86处理器(即80386之后)都有8个32位的通用寄存器(General Purpose Registers, GPR), GPR

  1. 通用寄存器(32位)
    • EAX:一般用作累加器(Accumulator)
    • EBX:一般用作基址寄存器(Base)
    • ECX:一般用来计数(Count)
    • EDX:一般用来存放数据(Data)
    • ESI:一般用作源变址(Source Index)
    • EDI:一般用作目标变址(Destinatin Index)
    • ESP:一般用作堆栈指针(Stack Pointer)
    • EBP:一般用作基址指针(Base Pointer)
  2. 段寄存器(16位)
    • CS:代码段寄存器
    • DS:数据段寄存器
    • SS:堆栈段寄存器
    • ES、FS 及GS:附加数据段寄存器 segment
  3. 程序计数器/指令计数器EIP,相对于CS
  4. 状态码EFLAGS

通常长的(32位)寄存器都可以被划分成小的读写块,如32位eax,16位ax,高8位ah,低8位al,注意nasm对标号大小写是敏感的,但对寄存器不会。

内存与地址模式

程序重定位

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

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

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

内存分段机制

将内存分段,程序只需管偏移地址/相对地址就好了 段地址:偏移地址

程序重定位通过设置代码段CS寄存器和数据段DS寄存器实现

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

两种典型情况

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

段的划分是自由的,它可以起始于任何16字节对齐的位置,也可以是任意长度,只要不超过64KB。 正是由于段的划分非常自由,使得8086的内存访问也非常随意。 同一个物理地址,或者同一片内存区域,根据需要,可以随意指定一个段来访问它。

访问内存

声明静态数据需要添加数据大小,包括1Byte的db,2B的dw,4B的dd,注意内存都是连续的,按字节(B)寻址

.DATA
var DB 64
var2 DB ? ; uninitialized byte
    DB 10 ; no label, location is var2 + 1.
X DW ?    ; 2B uninitialized
Y DD 30000;
str	DB 'hello',0; 6 bytes starting at str

如果要访问某一大小的内存,则通过添加修饰词byteworddword实现(注意nasm是不存储变量类型的),这里的字(word)通常就指16位,双字(double word)32位

mov byte[ebx], 2

函数调用(call)堆栈组织 stack convention

Caller规则

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

Callee规则

  1. ebp推入栈,将esp的值拷贝入ebp
    push ebp
    mov ebp, esp
    
  2. 分配局部变量,记住栈往下生长,如分配3个4B,则sub esp, 12
  3. 保存寄存器状态

函数调用例子如下

.486
.MODEL FLAT
.CODE
PUBLIC _myFunc
_myFunc PROC
  ; Subroutine Prologue
  push ebp     ; Save the old base pointer value.
  mov ebp, esp ; Set the new base pointer value.
  sub esp, 4   ; Make room for one 4-byte local variable.
  push edi     ; Save the values of registers that the function
  push esi     ; will modify. This function uses EDI and ESI.
  ; (no need to save EBX, EBP, or ESP)

  ; Subroutine Body
  mov eax, [ebp+8]   ; Move value of parameter 1 into EAX
  mov esi, [ebp+12]  ; Move value of parameter 2 into ESI
  mov edi, [ebp+16]  ; Move value of parameter 3 into EDI

  mov [ebp-4], edi   ; Move EDI into the local variable
  add [ebp-4], esi   ; Add ESI into the local variable
  add eax, [ebp-4]   ; Add the contents of the local variable
                     ; into EAX (final result)

  ; Subroutine Epilogue
  pop esi      ; Recover register values
  pop  edi
  mov esp, ebp ; Deallocate local variables
  pop ebp ; Restore the caller's base pointer value
  ret
_myFunc ENDP
END

常见指令

由于x86指令实在太多,这里只摘录一些常用的指令

  1. 通用数据传送
    • mov <ra>, <rb>
    • push <r>pusha:把AX,CX,DX,BX,SP,BP,SI,DI依次压入堆栈
    • pop <r>popa:把DI,SI,BP,SP,BX,DX,CX,AX依次弹出堆栈
  2. 算术逻辑运算
    • add <ra>, <rb>sub:ra+=rb
    • incdec
    • cmp <ra>, <rb>
    • mul <r>:等价于mul ax, <r>
    • andorxor
    • shlsalshrsar:h为逻辑,a为算术
  3. 无条件转移
    • jmp <label>
    • call <label>:过程调用
    • ret:过程返回
  4. 条件转移
    • je <label>jne:上一算术逻辑运算结果
    • jljg:小于大于
  5. 循环控制
    • loop:cx不为0时循环
  6. 中断指令
    • cli/sti:关中断/开中断
    • int <num>:中断
    • iret:中断返回

通过标号加方括号访问内存,如mov ax, [mem]

还有一些比较常见的特殊指令:

  • leave:等价于恢复堆栈,即mov esp,ebp\n\t pop ebp

nasm

The netwide assembler(NASM)是80x86和x86-64系列的汇编器

  • 指令格式:label: instruction operands ; comment nasm instruction format
  • 伪指令
message db 'hello, world'
msglen equ ($-message)

zerobuf: times 64 db 0 ; executed n times
  • 在nasm中支持多个变量的运算取址,如mov eax,[ebx*2+ecx+offset]
  • 字符串常量db 'hello'等价于db 'h','e','l','l','o',0
  • %define
%macro  prologue 1
    push  ebp
    mov   ebp,esp
    sub   esp,%1
%endmacro

参考资料

  1. Assembly Language Tutorial (x86), http://www.hep.wisc.edu/~pinghc/x86AssmTutorial.htm
  2. x86 Assembly Guide, http://www.cs.virginia.edu/~evans/cs216/guides/x86.html#instructions
  3. NASM Tutorial, http://cs.lmu.edu/~ray/notes/nasmtutorial/
  4. 为什么我们还在使用X86 CPU?致敬8086处理器问世40周年 - 老狼的文章 - 知乎 https://zhuanlan.zhihu.com/p/38002889
  5. 80386的各种寄存器一览, https://www.cnblogs.com/alantu2018/p/8471955.html
  6. Question: Compare 8086, 80386 and Pentium, http://www.ques10.com/p/13578/compare-8086-80386-and-pentium-1/
  7. 李忠,《x86汇编语言–从实模式到保护模式》
  8. org指令的作用,https://blog.csdn.net/mirage1993/article/details/29908929
  9. Stack frame layout on x86-64, https://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64/