汇编复习笔记
基础知识
1.从C语言到汇编语言
- 预处理:对C程序进行预处理,是可以打开的文本文件。
gcc -E main.c -o main.i
- 编译:将预处理的程序翻译成汇编语言。
gcc -S main.i -o main.s
- 汇编:
将编译生成的汇编语言代码翻译为机器语言代码。通常最终的可执行目标文件由多个不同模块对应的机器语言目标代码组合形成,所以,在生成单个模块的机器语言目标代码时,不可能确定每条指令或者每个数据最终的地址。——单个模块的机器语言目标代码需要重新定位,因此叫做“可重定位目标文件”。
gcc -C main.s -o main.o
-
dd :dword, sdword
dw:word,sword
db:byte,sbyte
dq:qword,sqword
2.x86的工作方式
- 实方式:32位CPU以16位CPU的工作方式工作。寻址1MB的物理存储空间,程序段大小不超过64KB,段基址和偏移地址均16位。
- 保护方式:32位CPU寻址4GB大小的物理存储空间,虚拟存储空间可达64TB。程序段大小4GB,段基址和偏移地址均32位。
3.标志寄存器
- 溢出判断:有符号数,OF=1(正常判断);无符号数,CF=1(只有下图中的最高位进位位)。则原始数据发生溢出。
-
加法溢出就是正正得负,负负得正。
-
减法要判断:1被减数与减数符号相异。
2 差与被减数符号相异。
两者都满足,即为溢出。OF=1。(正-负=负,负-正=正)
溢出(加法):字 32767(7FFF) + 3 = 32770(8002),符号标志位 OV=1,CF=1,输出32770.
例:
由于Cf = 1, Cn =1 ,则OF= 0。但CF= 1。
数据在机内的表示形式
-
示例一:
-
示例二:
s1 dw '12'; 存放32H 31H 注意:%s数据存储时,没有\0,且读出时,按照看到的从左往右读,但实际存储是从右往左存储。 s2 dw '1','2' ;存放31H 32H s3 dw '1' ;存放31H 00H
-
示例三:
x dw $ - x ;不占用内存的存放空间。 len = 5 ;不占用内存
-
实方式下(16位地址线)物理地址的形成:
在16位内存中只存地址的高16位,低4位在查找时,找到对应段寄存器的偏移地址,与段起始地址左移4位求和,得到物理地址。
4.寻址方式
寻址方式一共有6种:
-
立即寻址:操作数可以是立即数,也可以是一串未完成的加减乘除运算(但是算式中不能含有未知数)。立即寻址时,操作数就在操作指令的后面跟着。
mov al, -12H ; (al) = 0EEH
-
寄存器寻址:寄存器本质上,是为CPU中的存储单元一个助记符名,方便调用。其类型为dword,32位。寄存器寻址时,操作数就在CPU中存储,且这个存储单元有固定的名称(即为“寄存器”)。
add eax, ebx
-
存储器寻址:由于CPU的存储空间有限,不可能将所有的操作数和指令都存在CPU中。因此,在廉价且容量大的内存中,存储了大量的操作数。而从内存中寻址,就叫做存储器寻址。
-
直接寻址:操作数在内存中存放。操作数的偏移地址紧跟指令操作码,构成指令操作码的一部分。
mov ax, x + 2 ;将x的地址加2,得到新的地址。取word放入ax。
功能举例:
;将y的第二个字的内容改为50H .data y dw 10H, 20H, 70H, 80H .code ;法一:直接寻址 mov y + 4, 50H ;法二 mov eax, 2 shl eax, 1 mov [y的起始地址(dw) + eax], 50H
例:
mov BUF, 1
BUF是直接寻址。
-
-
寄存器间接寻址:操作数在存储器中,其全部的偏移地址在寄存器中。
mov eax, offset x + 1 ;立即寻址 mov byte ptr[eax], 32H ;寄存器间接寻址 ;或者 mov ax, [esi] ;目的操作数:寄存器寻址, 源操作数:寄存器间接寻址
-
变址寻址:操作数在存储器中,偏移地址的个数在寄存器中,使用时需要乘比例因子F(1,2,4,8),再与给定的起始地址相加。
F 是一个元素的长度。
mov ebx, 1 mov eax, y[ebx * 4] ;或者 mov ebx, offset y mov eax, [ebx + 8]
-
基址加变址寻址:操作数在存储器中,与变址寻址不同的是,增加一个基址寄存器,用来存放地址的基址。
x dd 10,20,30,40,50 dd 10,20,30,40,50 dd 10,20,30,40,50 imul ebx, i, 5 * 4 mov esi, j mov eax, x[ebx][esi * 4]
5.通用机器指令
总览
数据传输指令
-
movsx:有符号数扩展
-
movzx:无符号数扩展
-
xchg:数据交换,且两个数据不能同时为存储器寻址。
-
xlat:查表转换指令。其中,
[ebx + al] -> al
。tab db '0123456789ABCDEF' mov ebx, offset tab mov al, 10 xlat ;这段代码的功能:将tab中的'A'放入al中。
堆栈操作指令
存取方式:从高字节向低字节存储,存储一个字或者双字。
由于堆栈中存储的均为字或者双字,因此堆栈操作指令全部对于字或者双字,其他类型将要产生语法错误。
-
push
-
pop
-
lea R32,ops : ops一定是一个存储器地址。
-
pusha/popa:16位寄存器按照指定方式依次进出栈。
-
pushad/popad:32位寄存器按照指定方式依次进出栈。
算数运算指令
-
加法:adc:带CF位的加法。
例:用32位寄存器实现64位加法。
x dd 0F2345678H, 30010205H y dd 10000002H, 55667780H z dd 0, 0 mov eax, x add eax, y mov z, eax mov eax, x + 4 adc eax, y + 4 ;关键步骤:加低位进位。 mov z + 4, eax
-
减法:neg:求补
sbb:带CF位的减法,opd - ops - cf = opd
-
乘法:
;有符号乘指令:若乘积的高位不是低位的符号扩展,而是包含有效位,则OF=1,CF=1 imul ops (al) * ops -> ax (ax) * ops -> dx, ax (eax) * ops -> edx, eax ;无符号乘指令:同上,只是解释为无符号数。 mul ops
-
除法:有符号除和无符号除与乘法类似,其中al,ax,eax存放商,而ah, dx, edx存放余数
;除法溢出问题:(ax) = 1234, (bx) = 1 idiv ops;有符号除法 div ops;无符号除法
逻辑运算指令
- not:逐位取反。
- and:逻辑乘,就是“与”
- or:逻辑加,就是“或”
- xor:按位加,就是”异或“。
xor eax, eax
,则eax的值为0. - test:逻辑乘(and)以后,根据结果设置标志位,而源操作数和目的操作数的结果不改变。
符号扩展指令
- 字节→字:CBW
- 字→双字:CWD
移位指令
-
rol:循环左移,带CF位。CF为最后移入位。但移位时,不包含CF。0EAH->0AEH, CF=0
-
ror:循环右移,带CF位。CF为最后移入位。但移位时,不包含CF。0EAH->0AEH, CF=1
引申:交换高低字节:
-
rcl:带CF的循环左移。0EAH->0A7H, CF=0
-
rcr:带CF的循环右移。0EAH->0AEH, CF=1
转移指令
-
简单转移指令:js, jns, jz/je, jnz/jne, jo, jno, jc, jnc, jp/jpe(偶转移),jnp/jpo(奇转移)
-
无符号条件转移指令:ja/jnbe(ZF=0 & CF =0), jae/jnb, jb/jnae, jbe/jna
-
有符号条件转移指令:jg/jnle(OF=SF & ZF=0), jge/jnl, jl/jnge, jle/jng
-
无条件转移指令:
jmp: lea, ebx, BUF
jmp dword ptr[ebx]
-
分支循环指令
转移的范围在-128~+127之间。
- loop: ecx/cx - 1 -> ecx/cx,若不为0,转移到标志位继续。但注意:ecx倒计数不影响标志位
- loope:ecx/cx != 0 & ZF = 1,转到标号进行。否则终止。(也就是说,ZF要一直为1)
- loopne:ecx/cx != 0 & ZF = 0,转到标号进行。否则终止。(ZF == 0)
- jcxz: ecx/cx的值为0时,转到标号进行。否则终止。
6.子程序设计
什么是断点?
断点是, invoke 或者 call 语句的直接后继语句的地址。由于程序需要执行完子程序之后返回执行,因此断点要保存在esp中。
一个例子说明断点:
p1:call func1
func1 proc
...
ret
func1 endp
p2:...
call时,将func1的地址入栈,当ret时,弹出栈顶的dword送入EIP,子程序func1将再次执行,陷入不可控的情况。
地址
指令指示器:保存着下一条将要被CPU执行的指令的偏移地址(EA),值为下一条指令距离段首址的字节距离。
子程序的声明及调用
-
声明:
func1 proc NEAR/FAR C func1 endp
弹出时,以入栈顺序相反的顺序弹出。
-
调用:
table dd func1 call func1 call table
- call:1保存断点,2将子程序的地址送给EIP
- ret:栈顶弹出一个双字赋给EIP,并返回。这个双字存储的必须是断点地址。
- ret n: ↑(ESP) → EIP,(ESP)+n →ESP.消除入口参数对堆栈空间的调用。
-
注意:invoke / call时的机器码:函数地址-断点地址
子程序完整说明和调用
- 定义:
local u:dword, v:dword, w:dword
;使用
fun proc
local flag:dword
push ebx
mov ebx, [ebp + 8]
mov flag, ebx
pop ebx
ret
fun endp
-
子程序的完整定义:
func proc c/stdcall len:dword, LIST: dword
-
调用:
invoke func x, y call func
invoke 是伪指令。
传参方式:堆栈法
堆栈数据如何存放?
当转到子程序的入口执行时,堆栈中数据的摆放位置如图所示:
堆栈 | 说明 |
---|---|
局部变量2(第二个定义) | |
局部变量1(第一个定义) | [ebp - n] n为正数 |
断点地址(dw) | esp指向的位置(栈顶,esp指向位置) |
最后push的参数 | 小端存放 |
… | |
最先push的参数 | 栈底,ebp指向位置。 |
如果要访问push进入的参数,则需要移动esp。但是这样便会改变esp的指向,使得子程序退出时,不能将断点地址正确弹出送给eip。
原因是:‘ret’ 的功能:从堆栈栈顶弹出一个双字送给eip。初始时,esp指向断点地址的高位。
因此需要在子程序开始时,对esp进行保护。常用的保护如下:
push ebp
mov ebp, esp
;最后Push的参数访问:
mov eax, [ebp + 8]
;倒数第二个push的参数访问:
mov ebx, [ebp + 12]
局部变量
局部变量与函数参数的区别
局部变量与全局变量的区别:
局部变量 | 全局变量 | |
---|---|---|
地址获取 | 仅lea | lea, offset |
寻址方式 | 变址寻址[ebp + n] | 直接寻址 |
x[IR * F] | 等价于[IR * F + ebp + n] | 就是原形式 |
V[BR+IR * F] | 不能用,本身为变址寻址 | 可以用 |
7.多模块设计
-
extern: 在该模块使用但不在该模块定义的符号。
例如:
extern count:word
-
public:在该模块定义、且其他模块要使用的符号。
例如:
public func1 func1 proc x:dword, y:dword ... ret func1 endp
如果在CPP文件中调用,应该是:
extern "C" int func1(int x, int y);
extern "C" void sort(int *, int); ;sort为语言类型”C" extern "C" void cdecl sort(int *, int); ;sort为语言类型“stdcall" extern "C" void stdcall sort(int *, int);
注意:在C,C++中,函数名称区分大小写。
宏
-
定义:macro, endm
myadd macro x, y, z push eax mov eax, x add eax, y mov z, eax pop eax endm
-
调用:
.data x dd 1 y dd 2 z dd ? .code myadd x,y,z
-
扩展:将宏调用语句替换为宏体。并将实参按照一一对应关系替换形参。并对生成的语句进行语法检查。
在汇编时,由编译器处理。
在使用前,要先定义。
8.中断
-
中断源:引起中断的事件。
- 外部中断:来源于CPU之外的中断。(发生具有随机性)
- 不可屏蔽中断
- 可屏蔽中断:是否响应取决于eflags中的中断允许标志IF 位。
- IF = 1:响应中断。(sti)
- IF = 0:不响应中断。(cli)
- 内部中断(异常)
- 故障:cs:eip是要引起异常的指令的地址。
- 陷阱(发生不具有随机性):cs:eip是异常处理后要执行的指令的地址。
- 软中断:在程序中写了中断指令,执行该语句就回去调用中断处理程序,中断处理完后又继续运行下面的程序。
- 中止:在系统出现严重问题时,通知系统的一种异常。
- 正执行的程序不能被恢复执行。
- 外部中断:来源于CPU之外的中断。(发生具有随机性)
-
中断描述符可以化为三种门:
- 任务门:执行中断处理程序时,发生任务转移。
- 中断门:主要用于处理外部中断,响应中断时,自动将0->TF, 0->IF
- 陷阱门:主要用于处理异常,响应中断时,将0->TF
-
iret:在异常服务程序的末尾存在,实现从服务程序返回被中断的程序。弹出EIP, CS, EFLAGS。
-
int n:执行中断向量号为n的异常。
-
into:当OF = 1且 写出into指令时,执行int 4。
-
int 3:显式调用异常处理程序。不用设置断点即可在此处暂停运行。执行结束后,继续执行下一条指令。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VkSfRe03-1652525587642)(依托实验的汇编语言复习/image-20220425195022888.png)]
-
bound r, mem:r为寄存器,mem与r同类型的存储地址,该地址单元对应下边界,该地质单元对应的下一个单元内容为上边界。当r超过上下边界时,执行int 5。
指令 | 功能 | 备注 |
---|---|---|
seg intrr | 取段地址 | intrr:中断处理程序的入口标号或者过程名 |
offset intrr | 取偏移地址 | OFFSET INTRR ->0:[n*4], SEG INTRR->0:[n*4+2] |
STI | 开中断 | IF=1 |
CLI | 关中断 | IF=0 |
jmp dword opd | 16位段程序中的段间间接跳转 | (opd)->IP/EIP, (opd + 2)->CS |
call dword ptr opd | 16位段的段间间接调用 | (CS) →↓(SP/ESP) (IP/EIP)→ ↓(SP/ESP) **(OPD)→IP/EIP ** (OPD+2/4)→CS |
mov ax, 0
mov ds, ax
cli ;关中断
mov word ptr ds:[n * 4], offset intrr
mov word ptr ds:[n * 4 + 2], seg intrr
sti ;开中断
DOS软中断
系统功能调用
编程规范
-
输出一个字符串时,应该这样:
lpFmt db "%s", 0ah, 0dh, 0 arr db "Hello, world", 0 invoke printf, offset lpFmt, offset arr
-
若要定义有符号数,应该这样:
x sbyte -5 y sword ? z sdword ?
-
putchar函数:
putchar proto c :byte ;显示给定 ASCII 对应的字符 includelib libcmt.lib includelib legacy_stdio_definitions.lib
-
程序的”帽子“:
在写程序开始前写的一些东西,模型定义,函数调用,等。
.686P ;接受全部的Pentium Pro伪指令 .model flat, stdcall/c ;存储模型, 语言类型。 ;flat意为:代码和数据全部放在同一个4G空间内。 ;stdcall:语言类型。在子程序中恢复堆栈。 ret n ;c:语言类型。在由调用函数的程序(例如主程序)释放参数所占的空间。
需要注意:
- 对于stdcall:采用堆栈法转递参数,参数进栈顺序依次为:函数原型描述中最右边的参数最先入栈,最左边的参数最先出栈。被调用者在返回时释放参数占用的堆栈空间。如:ExitProcess
- 对于C语言函数例如printf,其语言类型为c。均需要由调用函数的程序释放参数所占用的空间。传参方式与stdcall相同。
-
函数定义语句中的语言类型与函数声明语句中的类型一致。
-
对于模型为flat,应选择NEAR。
-
push ebp mov ebp, esp ;最后Push的参数访问: mov eax, [ebp + 8] ;倒数第二个push的参数访问: mov ebx, [ebp + 12]``` ;在ret前,可以 mov esp, ebp pop ebp
-
LEAVE的使用:
leave 等效于: mov esp, ebp pop ebp
-
写一个不改变寄存器的printf宏:
Myprint macro A, B pushad invoke printf, A, B popad endm
若参数个数不定:
Myprint_1 macro A pushad invoke printf, A popad endm ;调用时: Myprint_1 <A,B,C>
考试时还涉及的东西
- 十六进制减法,标志位的设定。
- IN/OUT指令
- 分支跳转jmp
- 设计题是计算一个分数数组中各分数段的人数,比较直观。