汇编语言简介
本篇主要以Inter汇编语法为例。
通用寄存器
从这张表中我们可以总结一下:
- (知识)只有
EAX
、ECX
、EDX
、EBX
有8位寄存器。 - (技巧)16位寄存器名称就是去掉了32位寄存器的E、8位就是32位中间的字母加上
L
和H
。
题外话:看这个数值,从右到左,偏左的为高位;除特定值以外,汇编不区分大小写。
每一个内存地址(编号)都能存储一个字节(8位)。内存地址是32位的(32位表示32个0和 1,但我们一般使用16进制去表示,例如:0x00000000),程序独立存储的内存空间范围为:0 - 0xFFFFFFFF,能存储的位数就是(0xFFFFFFFF+1)*8,加一是因为0也算一位,乘八是因为每块内存可以存储一个字节(8位),转为十进制就是:34359738368 / 8 / 1024 / 1024 / 1024 = 4G(换算:1Byte = 8Bit, 1KB = 1024Byte, 1MB = 1024KB, 1GB = 1024MB)
MOV
这个指令也可以向内存中添加数据或从内存中获取数据:
MOV DOWRD PTR DS:[内存地址], 立即数
MOV DOWRD PTR DS:[内存地址], 32位通用寄存器
MOV 32位通用寄存器, DOWRD PTR DS:[内存地址]
需要注意的是如上所述指令中DOWRD为数据宽度,存储的数据需要与DOWRD数据宽度一致,当然这里也可以为BYTE、WORD,例如:
MOV WORD PTR DS:[内存地址], 立即数
内存地址的5种形式
内存地址表示有很多种形式:
形式一:立即数
// 读取内存的值
mov eax, dword ptr ds:[0x13FFC4]
// 向内存中写入数据
mov dword ptr ds:[0X13FFC4], eax
形式二:[寄存器]
// 读取内存的值
move ecx, 0x13FFD0
mov eax, dword ptr ds:[ecx]
// 向内存中写入数据
mov edx, 0x13FFD8
mov dword ptr ds:[edx], 0x87654321
形式三:[寄存器+立即数]
// 读取内存的值
mov ecx, 0x13FFD0
mov eax, dword ptr ds:[ecx+4]
// 向内存中写入数据
mov edx, 0x13FFD8
mov dword ptr ds:[edx+0xC], 0x87654321
形式四:[寄存器+寄存器*{1,2,4,8}]
// 读取内存的值
mov eax, 0x13FFC4
mov ecx, 0x2
mov edx, dword ptr ds:[eax+eax*4]
// 向内存中写入数据
mov eax, 0x13FFC4
mov ecx, 0x2
mov dword ptr ds:[eax+eax*4], 0x87654321
形式五:[寄存器+寄存器*{1,2,4,8}+立即数]
// 读取内存的值
mov eax, 0x13FFC4 mov ecx, 0x2
mov edx, dword ptr ds:[eax+eax*4+4]
// 向内存中写入数据
mov eax, 0x13FFC4
mov ecx, 0x2
mov dword ptr ds:[eax+eax*4+4], 0x87654321
存储模式(大、小端)
要在计算机中存储数据,需要遵循其的存储模式,存储模式分为两种:大、小端。
大端模式:数据高位在低位地址中,数据低位在高位地址中;
小端模式:数据低位在低位地址中,数据高位在高位地址中。
如下图中所示,内存地址从小到大;我们知道每一个内存地址可以存储8位,也就是一个字节,当我们使用MOV指令写入数据到内存中时指定宽度为BYTE存储数据会存储在一个内存地址中,而当我们指定数据宽度为WORD、DWORD呢?
汇编指令:
mov word ptr ds:[0x00000000], 0x1A2B
这里存储的是2个字节的数据,如果按照大端模式去存储,0x1A2B数据高位就是1A,低位就是2B,内存地址中从小到大,小的则是低位,高的则是高位,所以内存地址0x00000000存储1A,0x00000001存储2B;
小端模式则内存地址0x00000000存储2B,0x00000001存储1A。
常用汇编指令
我们常用的汇编指令有:MOV
、ADD
、SUB
、AND
、OR
、XOR
、NOT
......
如下格式举例中表示含义:
格式符 | 含义 |
---|---|
r | 代表通用寄存器 |
m | 代表内存 |
imm | 代表立即数 |
r8 | 代表8位通用寄存器 |
m8 | 代表8位内存 |
imm8 | 代表8位立即数 |
其他以此类推... |
MOV指令
表示数据传送,其格式为:
// MOV 目标操作数,源操作数
// 含义:将源操作数传送到目标操作数
MOV r/m8,r8
MOV r/m16,r16
MOV r/m32,r32
MOV r8,r/m
MOV r16,r/m16
MOV r32,r/m32
MOV r8, imm8
MOV r16, imm16
MOV r32, imm32
ADD指令
表示数据相加,其格式为:
// ADD 目标操作数,源操作数
// 含义:将源操作数与目标操作数相加,最后结果给到目标操作数
ADD r/m8, imm8
ADD r/m16,imm16
ADD r/m32,imm32
ADD r/m16, imm8
ADD r/m32, imm8
ADD r/m8, r8
ADD r/m16, r16
ADD r/m32, r32
ADD r8, r/m8
ADD r16, r/m16
ADD r32, r/m32
SUB指令
表示数据相减,其格式为:
// SUB 目标操作数,源操作数
// 含义:将源操作数与目标操作数相减,最后结果给到目标操作数
SUB r/m8, imm8
SUB r/m16,imm16
SUB r/m32,imm32
SUB r/m16, imm8
SUB r/m32, imm8
SUB r/m8, r8
SUB r/m16, r16
SUB r/m32, r32
SUB r8, r/m8
SUB r16, r/m16
SUB r32, r/m32
AND指令
表示数据相与(位运算知识),其格式为:
// AND 目标操作数,源操作数
// 含义:将源操作数与目标操作数进行与运算,最后结果给到目标操作数
AND r/m8, imm8
AND r/m16,imm16
AND r/m32,imm32
AND r/m16, imm8
AND r/m32, imm8
AND r/m8, r8
AND r/m16, r16
AND r/m32, r32
AND r8, r/m8
AND r16, r/m16
AND r32, r/m32
OR指令
表示数据相或(位运算知识),其格式为:
// AND 目标操作数,源操作数
// 含义:将源操作数与目标操作数进行或运算,最后结果给到目标操作数
OR r/m8, imm8
OR r/m16,imm16
OR r/m32,imm32
OR r/m16, imm8
OR r/m32, imm8
OR r/m8, r8
OR r/m16, r16
OR r/m32, r32
OR r8, r/m8
OR r16, r/m16
OR r32, r/m32
XOR指令
表示数据相异或(位运算知识),其格式为:
// XOR 目标操作数,源操作数
// 含义:将源操作数与目标操作数进行异或运算,最后结果给到目标操作数
XOR r/m8, imm8
XOR r/m16,imm16
XOR r/m32,imm32
XOR r/m16, imm8
XOR r/m32, imm8
XOR r/m8, r8
XOR r/m16, r16
XOR r/m32, r32
XOR r8, r/m8
XOR r16, r/m16
XOR r32, r/m32
NOT指令
表示非(位运算知识),其格式为:
// NOT 目标操作数
// 含义:将源操作数进行非运算,最后结果给到目标操作数
NOT r/m8
NOT r/m16
NOT r/m32
MOVS指令
表示数据传送,它与MOV的不同处在于,它可以将内存的数据传送到内存,但也仅仅能如此,其格式为:
// MOVS EDI指定的内存地址,ESI指定的内存地址
// 含义:将ESI指定的内存地址的数据传送到EDI指定的内存地址(使用MOVS指令时,默认使用的就是ESI和EDI寄存器),MOVS指令执行完成后ESI、EDI寄存器的值会自增或自减,自增或自减多少取决于传送数据的数据宽度
MOVS BYTE PTR ES:[EDI], BYTE PTR DS:[ESI] //简写为:MOVSB
MOVS WORD PTR ES:[EDI], WORD PTR DS:[ESI] //简写为:MOVSW
MOVS DWORD PTR ES:[EDI], DWORD PTR DS:[ESI] //简写为:MOVSD
MOVS指令举例说明:
- 先将ESI、EDI的值修改为对应内存地址
MOV ESI, 0x12FFC4
MOV EDI, 0x12FFD0
- 将0x11223344存入EDI指定的内存地址中
MOV DWORD PTR DS:[ESI], 0x11223344
- 使用简写方式指令:MOVSD,将ESI指定的内存地址的值(4字节数据)存入EDI指定的内存地址中
MOVS DWORD PTR ES:[EDI], DWORD PTR DS:[ESI]
- 结论:可以看到当MOVS指令执行结束后,
ESI
、EDI
的值自增了4,「自增多少取决于传送数据的数据宽度」,所以这里是DOWRD
表示4字节,也就自增了4。 - 注意:如果DF位为0,MOVS指令执行结束后,
ESI
、EDI
的值自增;如果DF位为1,MOVS指令执行结束后,ESI、EDI的值自减。(此块涉及标志寄存器:EFL,可以向下翻阅相关知识)
STOS指令
表示将AL/AX/EAX
的值储存到EDI指定的内存地址,其格式为:
// STOS EDI指定的内存地址
// 含义:将AL/AX/EAX的值储存到EDI指定的内存地址,STOS指令执行完成后EDI寄存器的值会自增或自减,自增或自减多少取决于传送数据的数据宽度,与MOVS指令一样自增或自减取决于DF位
STOS BYTE PTR ES:[EDI] //简写为:STOSB
STOS WORD PTR ES:[EDI] //简写为:STOSW
STOS DWORD PTR ES:[EDI] //简写为:STOSD
REP指令
表示循环,其格式为:
// REP MOVS指令/STOS指令
// 含义:循环执行MOVS指令或STOS指令,循环次数取决于ECX寄存器中的值,每执行一次,ECX寄存器中的值就会减一,直至为零,REP指令执行完成
REP MOVSB
REP MOVSW
REP MOVSD
REP STOSB
REP STOSW
REP STOSD
堆栈相关指令
在学习堆栈相关指令之前,我们需要知道什么是堆栈,实际上我们之前就提到过这个东西,堆栈就是前面我们提到的程序的独立4GB内存空间,与数据结构的堆栈无关。
聊到堆栈,我们又不得不提一下之前所学到的通用寄存器:ESP
、EBP
,其分别表示栈顶指针(表示当前正在使用的堆栈地址)、栈底指针(表示当前使用的堆栈的第一个地址),当前使用的堆栈地址范围就可以是ESP - EBP。
堆栈的地址使用是从大用到小的(高位地址到低位地址)
堆栈讲究先入后出的概念,也就是当我们先放A数据到堆栈中,再放B数据到堆栈中,当我们不使用这些数据时候就要“清理”,那么数据B就是先处理的,数据A就是后处理的。
我们想要往堆栈中压入数据就需要使用一个当前没有使用的堆栈地址避免覆盖了其他数据,也就是我们先存入数据,然后告诉堆栈当前用到了哪(提升栈顶指针ESP),在之前的章节中我们学习了使用MOV指令向内存中存储数据,在这里就可以借助MOV指令去压入数据:
mov dword ptr ds:[esp-4],0x11223344
sub esp, 0x4 // 由于堆栈地址使用是从大到小的,所以使用sub指令提升esp栈顶指针(4表示我们之前存储的数据宽度为4字节,所以栈顶指针ESP需要提升4)
而当我们不需要使用这个数据的时候,就要释放,同样我们使用汇编指令就可以完成:
add esp, 0x4 // 直接下降栈顶指针ESP
但是有一个问题,当我们把EAX的值压入堆栈中,然后再想把EAX的值放回去使用这种方法完全不行,我们还需要一个MOV指令将值放回去,这样很繁琐,所以我们可以使用堆栈指令:PUSH
(压入)和POP
(释放)。
PUSH指令
表示压入数据,其格式为:
// PUSH 通用寄存器/内存地址/立即数
// 含义:向堆栈中压入数据,压入数据后会提升(sub)栈顶指针(ESP),提升多少取决于压入数据的数据宽度
PUSH r16/r32
PUSH m16/m32
PUSH imm8/imm16/imm32
POP指令
表示释放数据,其格式为:
// POP 通用寄存器/内存地址
// 含义:释放压入堆栈中的数据,释放数据后会下降(add)栈顶指针(ESP),下降多少取决于释放数据的数据宽度
POP r16/r32
POP m16/m32
修改EIP的指令
EIP
也是寄存器,但它不叫通用寄存器,它里面存放的值是CPU下次要执行的指令地址;当我们想去修改它的值就不能使用修改通用寄存器那些指令了,修改EIP
有其特有的指令,接下来让我们来了解一下吧。
JMP指令
表示跳转,其格式为:
// JMP 寄存器/内存/立即数
// 含义:JMP指令会修改EIP的值为指定的指令地址,也就修改了程序下一次执行的指令地址,我们也可以称之为跳转到某条指令地址。
CALL指令
也可以修改EIP,跟JMP指令的功能是一样的,其格式为:
// CALL 寄存器/内存/立即数
// 含义:跟JMP指令的功能是一样的,同样也可以修改EIP的值,不同的点是,CALL指令执行后会将其下一条指令地址压入堆栈,ESP栈顶指针的值减4
// 注意:在我们使用DTDebug调试的时候,要跟进CALL指令不能使用F8要是用F7,这里涉及调试器相关的知识,暂时不深入了解
RET指令
表示返回,其格式为:
RET
// 含义:将当前栈顶指针的值赋给EIP,然后让栈顶指针加4
汇编眼中的函数
什么是函数?函数就是一系列指令的集合,为了完成某个会重复使用的特定功能。
那么在汇编中如何定义、使用函数呢?既然我们知道函数就是一系列指令的集合,那么只要我们随便写一段汇编代码即可:
mov eax, 0x1
我们将其写在了执行地址0x00460A32中,想要调用这个函数,需要使用JMP
、CALL
指令来调用。
但是我们一般不会使用JMP来调用函数,因为它执行完后没办法返回到原来要执行的地址,所以我们选择CALL
指令,CALL
指令需要搭配RET
指令一起使用。一般我们在函数指令集合的最后写入RET
指令,以此来实现函数执行完后返回原来要执行的地址继续执行。
那假设我们需要做一个任意两个数的加法函数该怎么办?这时候就需要想办法将我们的任意两个数传入函数中,这也就是参数;加法函数计算结果就称之为返回值。返回值在汇编中一般使用EAX存储,我们可以使用ECX、EDX作为传递参数,接下来我们编写加法函数:
add eax,ecx
add eax,edx
ret
堆栈传参
当函数有很多参数的时候,不止8个,那我们使用通用寄存器去传参,明显不够用,所以我们需要使用堆栈帮助我们传递参数。
堆栈平衡
我们知道当执行函数调用CALL指令的时候,会把CALL指令下一条指令的内存地址压入堆栈(ESP值减4);在函数内我们可以随意使用堆栈,比如PUSH指令压入堆栈,使用堆栈传参等等...
我们需要保证,在函数调用结束的时候(即执行RET指令之前,要把ESP栈顶指针的值修改为执行CALL指令压入堆栈或堆栈传参压入堆栈前的那个ESP栈顶指针的值),保证函数运行前与运行后ESP栈顶指针的值不变,这个我们称之为堆栈平衡。
- 第一种情况导致堆栈不平衡(函数内压栈):
函数内压栈会导致执行RET指令后,ESP-4,程序会到00000002这个执行地址继续执行,而不再是CALL指令下一行地址继续执行。
- 第二种情况导致堆栈不平衡(堆栈传参):
当RET指令之后,栈顶指针无法回到传参前的值。
平衡堆栈有两个方法:
- 外平栈:使用
ADD
指令。 - 内平栈:使用RET指令
对于第一种情况我们可以在函数内使用完堆栈后,POP释放数据;对于第二种堆栈传参导致的堆栈不平衡,我们可以使用外平栈、内平栈的方法。
ESP寻址
之前我们了解了函数,以及堆栈传参,那其实我们获取参数就是借助的ESP去获取对应参数的地址,这种行为我们称之为ESP寻址。
需要注意的是我们获取参数的值,指令应为:
mov eax, dword ptr ss:[esp+4]
你会发现原来指令中的ds变为了ss,这是因为当你的内存地址是esp或ebp组成的需要使用ss,暂时先不用管原因。
ESP寻址弊端:当函数比较复杂时,使用的时候要使用很多寄存器,需要把寄存器的值保存在堆栈中备份,寻址计算会复杂一些。
EBP寻址
之前都是借用ESP去寻址确定一些参数 ,但如果存到堆栈里面的值过多,那么就得不断地调整ESP的指向,这是ESP寻址的缺点。
那么EBP寻址的思路是什么呢?先把EBP的值保存起来,然后将EBP指向ESP的位置,接着在原来的堆栈基础上将ESP上移,重新变成一块新的堆栈;之后新的程序再使用堆栈的时候,只影响ESP但不会影响EBP,那我们寻址的时候使用EBP去寻址,EBP的位置相对固定,程序不管如何操作ESP都会不停浮动,但是EBP相对稳定。
标志寄存器
16位标志寄存器是:FLAGS
- 条件标志位:
SF
、ZF
、OF
、CF
、AF
、PF
CPU在执行完一条指令之后进行自动设置,反映了算数、逻辑运算等指令执行完毕之后,运算结果的特征。
- 控制标志位:
DF
、IF
、TF
控制CPU的运行方式和工作状态。
条件标志位
- 进位标志:【CF】—运算结果的最高有效位有进位(加法)或者借位(减法)。用于表示两个无符号数高低。
- 零标志:【ZF】—如果运算结果位0,则ZF=1,否则ZF=0
- 溢出标志位:【OF】—当将操作数作为有符号数的时候,使用该标志位判断运算结果是否溢出。加法:若相同符号数相加,结果的符号与之相反则OF=1,否则OF=0。
- 减法:若被减数与减数符号不相同,而结果的符号与减数相同则OF=1,否则OF=0发生溢出,说明运算的结果已经不可信
- 标志符号:【SF】—运算结果最高位为1,SF=1,否则SF=0。有符号数用最高有效位表示数据的符号,最高有效位是标志符号的状态。
- 奇偶标志位:【PF】—当运算结果(指的是低8位)中1的个数为偶数时,PF=1,否则PF=0。该标志位主要用于检测数据在传输过程中是否出错
- 辅助进位标志位:【AF】—一个字节运算的时候低4位向高4位的进位和错位。
注意
- 进位针对的是无符号数运算,溢出针对的是有符号数运算
- 进位了,可以根据CF标志位得到正确的结果,溢出了,结果已经不正确了。
- 汇编中的数据类型由程序员决定,也就是没有类型,程序员说是什么类型就是什么类型。所以当看到无符号数,则关注CF标志,看成有符号数,关注OF标志。
如何操作标志寄存器
LAHF(Load AH with flags)指令:用于将标志寄存器的低八位送入AH,即将标志寄存器FLAGS中的SF、ZF、AF、PF、CF五个标志位分别传送到AH的对应位(八位中有三位是无效的)
SAHF(store AH into flags)指令:用于将AH寄存器送入标志寄存器
PUSHF(push the flags)指令:用于将标志寄存器送入栈 → push eflags
POPF(pop the flags)指令:用于将标志寄存器送出栈 → pop eflags
JCC指令
JCC指令与标志寄存器是挂钩的,有太多了,不需要去背诵。
来自于滴水逆向论坛公开课