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
加法溢出就是正正得负,负负得正。
减法要判断: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位,低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]
movsx:有符号数扩展
movzx:无符号数扩展
xchg:数据交换,且两个数据不能同时为存储器寻址。
xlat:查表转换指令。其中,[ebx + al] -> al
。
tab db '0123456789ABCDEF'
mov ebx, offset tab
mov al, 10
xlat
;这段代码的功能:将tab中的'A'放入al中。
存取方式:从高字节向低字节存储,存储一个字或者双字。
由于堆栈中存储的均为字或者双字,因此堆栈操作指令全部对于字或者双字,其他类型将要产生语法错误。
加法: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;无符号除法
xor eax, eax
,则eax的值为0.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之间。
断点是, 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
注意: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] | 不能用,本身为变址寻址 | 可以用 |
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
扩展:将宏调用语句替换为宏体。并将实参按照一一对应关系替换形参。并对生成的语句进行语法检查。
在汇编时,由编译器处理。
在使用前,要先定义。
中断源:引起中断的事件。
中断描述符可以化为三种门:
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 ;开中断
输出一个字符串时,应该这样:
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:语言类型。在由调用函数的程序(例如主程序)释放参数所占的空间。
需要注意:
函数定义语句中的语言类型与函数声明语句中的类型一致。
对于模型为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