前言
感觉汇编挺有意思的,平时写代码的时候没考虑这么多,以下示例皆为8086CPU
8086 是 x86 架构的起点,后续的 80186、80286、80386、80486、Pentium 都是基于它扩展的
基础知识
存储器
8bit(位)=1Byte(字节)
2Byte=1word
1024Byte(字节)=1KB
存储器被划分为若干个存储单元,每个存储单元从0开始顺序编号,大小为1byte,表现为2位的16进制数
为方便调用,8086CPU在内部用段地址和偏移地址最终形成物理地址即
段地址*16(左移4位)+偏移地址=物理地址
大小端
示例
同样 int x = 0x12345678
大端序(Big Endian)
地址: 0x100 0x101 0x102 0x103
值: 12 34 56 78
小端序(Little Endian)
地址: 0x100 0x101 0x102 0x103
值: 78 56 34 12
这样看来大端更加贴近常识,但是常见的系统,如windows.linux等都是小端存储,所以遇到大于一个字节的数据,并且有操作字节的操作时应注意
python实现小端数据的运算
1 | cipher = bytearray(b"zer0pts") |
总线
CPU要对数据进行读写,必须和外部器件进行以下三类信息的交互:
1、存储单元的地址(地址信息);
2、器件的选择、读或写命令(控制信息);
3、读或写的数据(数据信息) 。
总线是连接CPU和其他芯片的导线,逻辑上分为地址总线、数据总线、控制总线。
CPU从内存单元中读写数据的过程:
1、CPU通过地址线将地址信息发出;
2、CPU通过控制线发出内存读命令,选中存储器芯片,并通知它将要从中读或写数据;
3、存储器将相应的地址单元中的数据通过数据线送入CPU或CPU通过数据线将数据送入相应的内存单元。
8086外部数据总线宽度为16位,它既能处理16位数据,也能处理8位数据。可寻址的内存空间为1mb。
CPU是通过地址总线指定存储单元,地址总线传送的能力决定了CPU对存储单元的寻址能力。
8086CPU有20位地址总线,可以传送20位地址,达到1MB寻址能力,但8086CPU内部数据总线宽度是16位,表现出来的寻址能力只有64kb。8086CPU采用一种在内部用用两个16位地址合成的方法来形成一个20位的物理地址。
寄存器
寄存器是cpu完成操作的载体
8086 CPU 中寄存器总共为 14 个,且均为 16 位。
即 AX,BX,CX,DX,SP,BP,SI,DI,IP,FLAG,CS,DS,SS,ES 共 14 个。而这 14 个寄存器按照一定方式又分为了通用寄存器,控制寄存器和段寄存器。
如果32位寄存器,就是在前面加E然后是32位,4字节
通用寄存器
通用寄存器的每一个16位寄存器就可以当做2个独立的8位寄存器来使用,如AX寄存器可以分为两个独立的8位的AH和AL寄存器;
- AX (Accumulator)——累加寄存器,也称之为累加器;
AX寄存器还具有的特殊用途是在使用DIV和MUL指令时使用,
在mul使用时al或ax作为被乘数,积保留在ax或积低16位保留在ax
在div使用时ax作为被除数或被除数后16位,商保留在al或ax - bx——based register——基地址寄存器
BX主要还是用于其专属功能–寻址(寻址物理内存地址)上,BX寄存器中存放的数据一般是用来作为偏移地址使用的。 - cx——count register——计数器
当在汇编指令中使用循环LOOP指令时,可以通过CX来指定需要循环的次数 - dx——data registered——数据寄存器
dx可与ax配合使用,常作为ax的扩展
在执行MUL指令时,如果两个相乘的数都是16位的话,那么相乘后产生的结果显然需要32位来保存,而这32位的结果的高16位就是存放在DX寄存器中。
如除数为16位时,被除数将会是32位,而被除数的高16位就是存放在DX中,而且执行完DIV指令后,本次除法运算所产生的余数将会保存在DX中段寄存器
cs——code segment——存储代码段的起始地址
ds——data segment——存储数据段的起始地址
ss——stack segment——存储栈段的起始地址
es——extra segment——附加段寄存器变址寄存
si——source index——源变址寄存器
di——destination index——目的变址寄存器指针寄存器
ip——instructor point——指令指针寄存器
sp——stack point——堆栈指针寄存器,始终指向当前栈顶
bp——base point——基础指针,在函数执行期间固定,作为当前函数栈帧的基准点
状态标志寄存器
| 位 | 名称 | 全称(英文) | 中文解释 | 为 1 时的简写 | 为 0 时的简写 | 说明 |
|---|---|---|---|---|---|---|
| 11 | OF | Overflow Flag | 溢出标志 | OV (Overflow) | NV (No Overflow) | 有符号数溢出时置 1 |
| 10 | DF | Direction Flag | 方向标志 | DN (Down) | UP (Up) | 字符串操作方向控制 |
| 9 | IF | Interrupt Flag | 中断允许标志 | EI (Enable Interrupt) | DI (Disable Interrupt) | 控制外部中断开关 |
| 8 | TF | Trap Flag | 陷阱标志(单步调试) | ST (Single-step Trap) | NT (No Trap) | 若为 1 → 每执行一条指令触发调试中断 |
| 7 | SF | Sign Flag | 符号标志 | NG (Negative) | PL (Plus) | 结果最高位为 1(负)时置 1 |
| 6 | ZF | Zero Flag | 零标志 | ZR (Zero) | NZ (Not Zero) | 运算结果为 0 时置 1 |
| 4 | AF | Auxiliary Carry Flag | 辅助进位标志 | AC (Auxiliary Carry) | NA (No Auxiliary Carry) | 用于 BCD 半字节进位 |
| 2 | PF | Parity Flag | 奇偶标志 | PE (Parity Even) | PO (Parity Odd) | 结果最低字节中 1 的个数为偶数则置 1 |
| 0 | CF | Carry Flag | 进位标志 | CY (Carry) | NC (No Carry) | 无符号进位/借位产生时置 1 |
汇编指令
数据传送指令
数据传送指令可以利用立即数和寄存器来完成存储单元的数据传输,没有程序转移的功能,不能改变cs:ip的值
通用数据传送指令。
MOV 传送指令 MOV DEST,SRC
XCHG 交换指令 XCHG OPER1,OPER2堆栈和堆栈操作指令。
PUSH 进栈指令 PUSH SRC
POP 出栈指令 POP DEST目的地址传送指令。
LEA 取偏移地址指令 LEA REC,OPRD
LDS 取指针内容指令 例: LDS SI,string ;把段地址:偏移地址存到DS:SI.
LES 取指针内容指令 例: LES DI,string ;把段地址:偏移地址存到ES:DI.符号拓展指令
CBW 字节转化为字指令 把寄存器AL中的值符号拓展到寄存器AH
CWD 字转化为双字指令 把寄存器AX中的值符号拓展到寄存器DX算术运算指令
加减
ADD 加法指令 ADD DEST,SRC DEST<=DEST SRC 两数相加
SUB 减法指令 SUB DEST,SRC DEST<=DEST-SRC 两数相减
INC 加1指令 INC DEST DEST<=DEST 1
DEC 减1指令 DEC DEST DEST<=DEST-1
ADC 带进位加法指令 ADC DEST,SRC DEST<=DEST SRC CF 与add指令不同之处是要再加上进位标志cf的值
SBB 带借位减法 SBB DEST,SRC DEST<=DEST-(SRC CF) 与sub指令不同之处是要再减上借位标志cf的值乘除
MUL 无符号数乘法指令
IMUL 有符号数乘法指令
DIV 无符号数除法指令
IDIV 有符号数除法比较
CMP 比较指令 CMP DEST,SRC 根据dest-src的差影响各状态标志寄存器 不把dest-src的结果送入dest相反数
NEG 取补指令 NEG OPRD OPRD=0-OPRD 对操作数取补(相反数)逻辑运算指令
| 指令 | 名称 | 格式 | 功能描述 | 运算规则 / 特点 |
|---|---|---|---|---|
| NOT | 否运算指令 | NOT OPRD |
把操作数 OPRD 按位取反,然后送回 OPRD | 每一位取反:1→0,0→1 |
| AND | 与运算指令 | AND DEST, SRC |
把两个操作数进行按位与运算,结果送回 DEST | 同 1 得 1,否则得 0 |
| OR | 或运算指令 | OR DEST, SRC |
把两个操作数进行按位或运算,结果送回 DEST | 同 0 得 0,否则得 1 |
| XOR | 异或运算指令 | XOR DEST, SRC |
把两个操作数进行按位异或运算,结果送回 DEST | 相同得 0,不同得 1 |
| TEST | 测试指令 | TEST DEST, SRC |
将两个操作数按位 AND 运算,但结果不保存,仅影响标志位 | 若结果为 0 → ZF=1;结果非 0 → ZF=0;同时 CF=0,OF=0,ZF/PF/SF 根据结果设置 |
| SAL | 算术左移 | SAL OPRD, count |
把操作数 OPRD 左移 count 位,右边补 0 | 与 SHL 相同;移位数由 count 低 5 位决定(0~31) |
| SHL | 逻辑左移 | SHL OPRD, count |
把操作数 OPRD 左移 count 位,右边补 0 | 与 SAL 相同;移位数取 count 低 5 位(0~31) |
| SAR | 算术右移 | SAR OPRD, count |
把操作数 OPRD 右移 count 位,左边补符号位,移出的最低位进 CF | 保持符号(正负不变);count 低 5 位为有效位数(0~31) |
| SHR | 逻辑右移 | SHR OPRD, count |
把操作数 OPRD 右移 count 位,左边补 0,移出的最低位进 CF | 无符号右移;count 低 5 位为有效位数(0~31) |
| ROL | 左循环移位 | ROL OPRD, count |
操作数按位循环左移 | 移出高位循环补入低位;count 低 5 位为有效位数(0~31) |
| ROR | 右循环移位 | ROR OPRD, count |
操作数按位循环右移 | 移出低位循环补入高位;count 低 5 位为有效位数(0~31) |
| RCL | 带进位左循环移位 | RCL OPRD, count |
操作数与 CF 一起参与循环左移 | CF 参与循环(扩展一位);count 低 5 位为有效位数(0~31) |
| RCR | 带进位右循环移位 | RCR OPRD, count |
操作数与 CF 一起参与循环右移 | CF 参与循环(扩展一位);count 低 5 位为有效位数(0~31) |
补充说明
- count 取值:CPU 实际只使用
count的低 5 位(即最多 31 次移位)。 - 标志位影响:
AND,OR,XOR,TEST:影响 ZF, SF, PF,清除 CF, OFSHL,SHR,SAL,SAR:影响 CF 和 OFROL,ROR,RCL,RCR:影响 CF 和 OF(依情况)
程序转移指令
转移指令
JMP 无条件段内直接转移指令 JMP LABEL 使控制无条件地转移到标号为label的位置 无条件转移指令本身不影响标志 JA/JNBE > JAE/JNB ≥ JB/JNAE < JBE/JNA ≤ 以上四条,测试无符号整数运算的结果(标志C和Z)。 JG/JNLE > JGE/JNL ≥ JL/JNGE < JLE/JNG ≤ 以上四条,测试有符号整数运算的结果(标志S,O和Z)。 JE/JZ ZF=1 JNE/JNZ ZF=0 JC CF=1 JNC JO OF=1 JNO JS SF=1 JNS
- 循环指令
LOOP 计数循环指令 LOOP LABEL 使CX的值减1,当CX的值不为0的时候跳转至LABEL,否则执行LOOP之后的语句 - 子程序指令
CALL 过程调用指令 CALL LABEL 段内直接调用LABEL 与jmp的区别在于call指令会在调用label之前保存返回地址(call 中return之后主程序还可以继续执行,jmp 当label执行完毕后不能返回主程序继续执行) RET 段内过程返回指令 RET 使子程序结束,继续执行主程序变量属性
SEG 得到变量名或标号名的段基址
OFFSET 得到变量名或标号名的偏移量
TYPE 在变量名前,返回值为1(字节)、2(字)、4(双字)
LENGTH 用在变量前面,对于变量使用DUP进行定义的情况,汇编程序将回送分配给该变量的单元数。对于其他情况则回送1
SIZE 用在变量前面,汇编程序将回送分配给该变量的字节数,值为 LENGTH 与 TYPE 值的乘积
PTR 指定类型汇编程序
一个汇编语言程序从写出到最终执行的简要过程:
编写 -> 编译连接 -> 执行
在Win 64位系统下实现Debug汇编的方法。
1 | mount c d:\ 回车 |
如果源程序没有错误,则自动生成.OBJ 文件和.EXE 可执行文件。
结构
1 | assum cs:codesg |
segment和ends是一对成对使用的伪指令,这是在写可被编译器编译的汇编程序时,必须要用到的一对伪指令。segment和ends的功能是定义一个段,segment说明一个段开始,ends 说明一个段结束。
assume对除了CS以外的其它段寄存器,仅仅只是关联了段名,以便在访问段内变量时程序可以知道用哪个段寄存器,并没有在程序加载时将段地址装入段寄存器。所以,将段地址装入段寄存器的工作,必须由用户在程序中自己编写代码,并在程序开始运行时执行代码完成装入工作。
内存并没有分段,段的划分来自于CPU。由于8086CPU用“基础地址(段地址×16)+偏移地址=物理地址”的方式给出内存单元的物理地址,使得我们可以用分段的方式来管理内存。
数据段:存放数据的段。使用时候,用DS寄存器。
程序段:用来存放程序的段。使用的时候,用CS和IP寄存器。
栈段: 是一个栈。使用时,初始设置SS和SP寄存器。
输入
字符输入
mov ah, 1;输入后,收到的值存放在AL中
int 21h字符串输入
BUFFER DB 20 ;预定义20字节的空间DB ? ;待输入完成后,自动获得输入的字符个数 DB 20 DUP(0) LEA DX,BUFFER ;接收字符串 MOV AH, 0AH INT 21H
输出
- 字符输出
1
2
3
4
5
6mov ah,02h
mov dl,0dh ;回车键
int 21h
mov ah,02h
mov dl,0ah ;换行键
int 21h - 字符串输出
1
2
3
4
5INPUT DB "Please input a string: ",'$'
LEA DX, INPUT ;打印提示输入信息
MOV AH, 09H
INT 21H