汇编学习
汇编语言
vscode反汇编
-exec disassemble /m -exec disassemble /m main -exec info registers //寄存器信息
字节长度
1byte=8bits
1word=2bytes=16bits
1dword=4bytes=32bits
逻辑运算
运算 | and | or | xor |
---|---|---|---|
0 0 | 0 | 0 | 0 |
0 1 | 0 | 1 | 1 |
1 0 | 0 | 1 | 1 |
1 1 | 1 | 1 | 0 |
计算机加法运算
以2+3为例
X:0010 Y:0011
1.首先进行xor运算得到0001(此操作将相加后无需进位的数保留下来)。
2.然后进行and运算得到0010,并左移一位 0010 << 1 ==0100 (此操作为进位操作) 。
3.最后判断2操作后得到的数是否为0(判断是否仍需进位)。
4.继续将1和2步骤得到的数进行上述操作,直到得到0为止 。
32位通用寄存器
寄存器 | 作用 |
---|---|
EAX | 累加器 |
ECX | 计数 |
EDX | I/O指针 |
EBX | DS段的数据指针 |
ESP | 堆栈指针 |
EBP | SS段的数据指针 |
ESI | 字符串操作的源指针:SS段的数据指针 |
EDI | 字符串操作的目标指针:ES段的数据指针 |
通用寄存器使用
MOV /ADD/SUB
MOV EAX,12345678 ;将12345678放入EAX中 ADD EAX,1 ;EAX中的值加1 MOV ECX,2 ;将2放入ECX中 ADD EAX,ECX ;将EAX和ECX中的值相加 SUB EAX,3 ;EAX中的值减3
LEA 寻址 LEA EAX,DWORD PTR DS:[0x12345678]
内存
1.每个内存单元的宽度为8。
2.[0x0000000] [0xFFFFFFFF]称为地址。
3.地址的作用:当我们想从内存中读取数据或者想向内存中写入数据,首先应该找到读、写的位置。
内存的读写
寻址公式 | 例子 |
---|---|
[立即数] | DWORD PTR DS:[0x12345678] |
[reg] reg代表任意寄存器 | DWORD PTR DS:[ECX] |
[reg+立即数] | DWORD PTR DS:[ECX+4] |
[reg+reg*{1,2,4,8}] | mov dword ptr ds[0x0000FFFF],0x0000FFFF |
[reg+reg*{1,2,4,8}+立即数] | DWORD PTR DS:[ECX+ECX*4+2] |
堆栈
PUSH ;进栈 POP ;出栈
先进栈后出栈,栈底地址不变。
栈
栈有两种基本操作:入栈和出栈。入栈就是将一个新的元素放到栈顶,出栈就是从栈顶取出一个元素。栈顶的元素总是最后入栈,需要出栈时,又最先从栈中取出。栈的这种操作规则被称为:LIFO(Last In First Out)。
最基本的两个指令是PUSH(入栈)和POP(出栈)。8086CPU的入栈和出栈操作都是以字为单位进行的。
8086CPU中,有两个寄存器,段寄存器SS和寄存器SP,栈顶地址存在SS中,偏移地址存在SP中。任意时刻SS:SP指向栈顶元素。push指令和pop指令执行时,CPU从SS和SP中得到栈顶地址。
POP指令与PUSh恰好相反。
栈顶超界问题
我们在编程的时候要自己操心栈顶超界的问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致的超界;执行出栈操作的时候也要注意,以防栈空的时候继续出栈而导致的超界。
push和pop
push和pop可以寄存器和内存之间传送数据
push 寄存器 ;寄存器中数据入栈 pop 寄存器 ;寄存器中数据出栈 push 段寄存器 pop 段寄存器 push 内存单元 ;将一个内存单元处的字入栈 pop 内存单元 ;出栈,用一个内存字单元接收出栈数据
mov ax,1000H mov ds,ax ;内存单元的段地址放入ds中 push [0] ;将1000:0处的字压入栈中 pop [2] ;出栈,出栈数据送入1000:2处
汇编语言
宽度:能存储的二进制数的位数。
逻辑运算:2-3
2:0010
-3:补码形式 1011->1100->1101
0010 xor 1101 = 1111
0010 and 1101 = 0000 << 0000
结果为1111 为-1的补码形式。
八进制计算2-5

这与存储器的宽度有关,可以看到在八进制结果中得到17·····75,这是因为在十六进制表达中,在这个64位存储器中,以二进制计算后的结果转为16进制就是上述F····FD(-3的补码形式),转为八进制就是上述结果。
寄存器和入栈出栈
运行代码
运行前寄存器情况
栈情况
PUSH EBP
:将EBP中的栈底地址压入栈,栈顶地址-4。

MOV EBP,ESP
:将栈顶地址放入EBP中,此时栈顶栈底地址相同
SUB ESP,40
:ESP中值减-0x40
PUSH EBX
:将ebx中存的值入栈且栈顶地址-4
PUSH ESI
:将ESI中存的值入栈且栈顶地址-4
PUSH EDI
:将EDI中存的值入栈且栈顶地址-4
LEA EDI,DWORD PTR SS:[EBP-40]
:EBP的地址值-0x40,存入EDI中
MOV ECX,10
:0x10存入ECX中
MOV EAX,0xCCCCCCCC
:
REP STOS DWORD PTR ES:[EDI]
:覆盖从edi开始的内存为eax,edi减小4。循环ecx次,每循环一次ecx的值减1。

MOV EAX,DWORD PTR SS:[EBP+8]
:ebp的地址+0x8中的值存入eax

ADD EAX,DWORD PTR SS:[EBP+C]
:ebp+0xc内的值加eax中的值,存入eax。
POP EDI
:栈顶地址中值取出,存入edi
POP ESI
:
POP EBX
:
MOV ESP,EBP
:
POP EBP
:
RETN
:retn表示pop eip,然后跳转到eip中的地址指向的指令准备开始执行
通用寄存器
寄存器 | 编号(二进制) | 编号(十进制) | ||
---|---|---|---|---|
32位 | 16位 | 8位 | ||
EAX | AX | AL | 000 | 0 |
ECX | CX | CL | 001 | 1 |
EDX | DX | DL | 010 | 2 |
EBX | BX | BL | 011 | 3 |
ESP | SP | AH | 100 | 4 |
EBP | BP | CH | 101 | 5 |
ESI | SI | DH | 110 | 6 |
EDI | DI | BH | 111 | 7 |
32位寄存器的前半部分为16位寄存器,前四个16位寄存器又可以分成8个8位寄存器。
验证如下
汇编指令
(r代表通用寄存器,m代表内存,imm代表立即数)
1.mov 目标操作数,源操作数
作用:拷贝源操作数到目标操作数
(1)源操作数可以是立即数、通用寄存器、段寄存器或内存单元
(2)目标操作数可以是通用寄存器、段寄存器、内存单元
(3)操作数宽度必须一样
(4)源操作数和目标操作数不能同时为内存单元
MOV r8/m8,r8 #可以将8位通用寄存器中的值存入8位通用寄存器或者8位内存中 MOV r16/m16,r16 MOV r32/m32,r32 MOV r8,r8/m8 #可以将8位通用寄存器或者8位内存中的值存入8位寄存器中 MOV r16,r16/m16 MOV r32,r32/m32 MOV r8,imm8 #将一个8位的立即数存入8位通用寄存器中 MOV r16,imm16 MOV r32,imm32
2.add 目标操作数,源操作数
作用:将目标操作数与源操作数相加,结果存入目标操作数
(1)源操作数可以是立即数、通用寄存器、段寄存器或内存单元
(2)目标操作数可以是通用寄存器、段寄存器、内存单元
(3)操作数宽度可以不一样
(4)源操作数和目标操作数不能同时为内存单元
ADD r8/m8,imm8 ADD r16/m16,imm16 ADD r32/m32,imm32 ADD r16/m16,imm8 #源操作数与目标操作数可以不一致!! ADD r32/m32,imm8 ADD r8/m8,r8 ADD r16/m16,r16 ADD r32/m32,r32 ADD r8,r8/m8 ADD r16,r16/m16 ADD r32,r32/m32
3.sub
作用:将目标操作数减源操作数,结果存入目标操作数
(1)源操作数可以是立即数、通用寄存器、段寄存器或内存单元
(2)目标操作数可以是通用寄存器、段寄存器、内存单元
(3)操作数宽度可以不一样
(4)源操作数和目标操作数不能同时为内存单元
SUB r8/m8,imm8 #8,16,32位都可以 SUB r8/m32,imm32 #源操作数与目标操作数宽度可以不一致 SUB r32/m32,r32 SUB r32,r32/m32
4.and/or/xor
AND r8/m8, imm8 AND r16/m16,imm16 AND r32/m32,imm32 AND r16/m16, imm8 AND r32/m32, imm8 AND r8/m8, r8 AND r16/m16, r16 AND r32/m32, r32 AND r8, r8/m8 AND r16, r16/m16 AND r32, r32/m32
5.not
NOT r8/m8 NOT r16/m16 NOT r32/m32
6.push 入栈
PUSH r32 PUSH r16 PUSH m16 PUSH m32 PUSH imm8/imm16/imm32 #举例: push eax push ax push word ptr ss:[esp] #会从低位开始往高位取16位,即四个八进制,剩下的就不取了 push dword ptr ds:[edx] push 0x12/0x1234/0x12345678/0x1 #这些数都会被当成32位立即数
7.pop 出栈
POP r32 POP r16 POP m16 POP m32 #举例: pop esi pop si pop word ptr ss:[esp] #注意一定不能写成sp,虽然sp是16位寄存器,但是ss:[]格式不允许16位,只允许32位寄存器,至于取多少内存宽度,取决的前面的word还是dword还是byte! pop dword ptr ss:[eax]
内存
mov dword ptr ds:[0x0012FF34],0x12345678
- dword表示要使用的内存大小为4个字节,即要读/写多少;还可以是byte和word等
- ptr就表示代表后面是一个指针,即后面要跟内存编号(固定写法)
- DS是段寄存器,32位汇编中段寄存器和内存的属性有关系:如果后面地址直接用数表示,则用DS;如果后面写ESP或者EBP,就写SS;如果EDI的话就用ES。所以32位汇编中段寄存器就是后面内存属性的一个标识。
- [0x0012FF34]表示后面的立即数存入内存的(起始)地址,如果前面是dword,表示从这个内存地址对应的一字节内存块开始存储(或读取),一共要存储(或读取)4字节内存,即4个内存块(因为一个地址对应一字节大小内存,那么一个编号可能不够存),即从[0x0012ff34]这个单元块开始存(或读),再往高地址存,依次往下,直到占用了4字节的内存,即从[0x0012ff34]到[0x0012ff37]都是用来存这个指令后面的数值的
- 0x12345678就是存入内存的数值,称为立即数。如果立即数的宽度比前面指定的内存宽度大,则从低位开始取,取到指定的宽度,高位多余的部分就被丢弃,比如要写入dword,但是立即数为0x123456789,则最终写入的内存的值为0x23456789,一共4字节
标志寄存器
一、标志寄存器总览
EFLAGS寄存器,主要用于反映处理器的状态和ALU运算结果的某些特征及控制指令的执行。通俗来讲:标志寄存器与CPU中的其它寄存器不一样:它是按位起作用的,每一位都有专门的含义,记录特定信息
二、标志寄存器常用标志
1.进位标志CF(Carry Flag)
运算最高位产生了进位或者错位,其值为1,否则为0。
;最高位取决于运算数据宽度 add eax,0x1 ;计算32位 add al,0xff ;计算8位
2.奇偶标志PF(Parity Flag)
运算结果的低八位中,(二进制)1的个数为偶数置1,为奇数置0。
只判断运算,mov操作不会改变PF。
3.辅助进位标志AF(Auxiliary Carry Flag)
无论数据宽度,看低四位是否发生进位或借位。如果发生进位或借位则值为1,否则为0。
4.零标志ZF(Zero Flag)
用来反映运算结果是否为0,为0则值为1,否则为0。
mov eax,0 ;操作并不会使ZF值变为1。
5.符号标志SF(Sign Flag)
符号标志SF用来反映运算结果的符号位,它与运算结果的最高位相同
注意如果SF位为1,只能说明最高位为1,不能说明这就一定是一个负数,因为有符号数和无符号数是程序员在写的时候自己决定 的,所以当程序员认为结果为无符号数时,那么最高位是1也不是负数,除非认为是有符号数,才能根据SF位为1判断是结果是负数
6.溢出标志OF(Overflow Flag)
用来反映有符号数加减所得结果是否溢出,如果运算结果超出当前运算位数所能表示的范围,则称为溢出,OF值为1,否则为0。(主要给有符号数运算使用)
无符号数相加超过数据宽度则溢出(一般看CF)
正+正=正 ,如果为负则有溢出
负+负=负 ,结果为正则有溢出
正+负 永远不会有溢出
7.方向标志DF(Direction Flag)
当该位置1时(DF=1),存储器地址自动减少(减多少一般看指令给的宽度是byte,word还是dword,或者视情况而定),串操作指令为自动减量指令,即从高位到低位处理字符串;当该位置0时(DF=0),存储器地址自动增加,串操作指令为自动增量指令。
其他汇编指令
1.ADC:带进位加法
相加运算后加上CF的值。
ADC R/M,R/M/IMM #两边不能同时为内存,且宽度要一样 #举例: ADC AL,CL ADC BYTE PTR DS:[12FFC4],2 ADC BYTE PTR DS:[12FFC4],AL
2.SBB:带借位减法
相减运算后减去CF的值。
SBB R/M,R/M/IMM #两边不能同时为内存,宽度要一样 #举例: SBB AL,CL SBB BYTE PTR DS:[12FFC4],2 SBB BYTE PTR DS:[12FFC4],AL #宽度一致
3.XCHG:交换数据
XCHG R/M,R/M #后面不能是立即数!两边不能同时为内存,宽度要一样 #举例: XCHG AL,CL XCHG DWORD PTR DS:[12FFC4],EAX XCHG BYTE PTR DS:[12FFC4],AL #宽度一致
4.MOVS:内存和内存之间移动数据
将后面ESI中存到内存地址编号中的值(按照给定的数据宽度),存到前面的EDI中的内存地址编号中。
DF为0时,ESI和EDI中的地址编号根据数据宽度进行+1/+2/+4
DF为1时,ESI和EDI中的地址编号根据数据宽度进行 -1/ -2/ -4
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
5.STOS:将AL/AX/EAX的值存到[EDI]指定的内存单元(根据数据宽度)
DF的值对EDI的影响同MOVS
STOS BYTE PTR ES:[EDI] #简写为 STOSB STOS WORD PTR ES:[EDI] #简写为 STOSW STOS DWORD PTR ES:[EDI] #简写为 STOSD
6.REP:按计数寄存器(ECX)中指定的次数重复执行,执行一次ECX-1
对象为MOVS和STOS
JCC
一、jmp
作用:修改EIP的值,执行时只有EIP变化。
EIP:CPU读取指令的地址。
jmp 立即数/寄存器
二、call
作用:修改EIP的值,将call下一跳指令的地址压入栈中。ESP会发生变化,此时栈顶地址也叫返回地址。
call 地址/寄存器
三、retn
作用:修改EIP的值,将栈顶地址中的值(返回地址)出栈,并赋给EIP,之后跳转到EIP中的地址,也就是call下面的操作。(通常与call一起使用)
四、cmp
作用:只改变标志寄存器,不修改操作数。
原理:进行一次SUB操作,但是并不修改寄存器中的值。根据SUB的结果来改变标志寄存器的值。当两个数相等时,ZF置1。
格式:
CMP EAX,ECX CMP AX,WORD PTR DS:[405000] #可以将内存中的值与寄存器作比较,但是数据宽度必须一致 CMP EAX,DWORD PTR DS:[405000]
cmp执行后通过标志寄存器的值判断两个数的大小。
无符号数:
CMP结果 | ZF | CF |
---|---|---|
目的操作数<源操作数 | 0 | 1 |
目的操作数>源操作数 | 0 | 0 |
目的操作数=源操作数 | 1 | 0 |
有符号数:
CMP结果 | 标志位 |
---|---|
目的操作数<源操作数 | SF≠OF |
目的操作数>源操作数 | SF=OF |
目的操作数=源操作数 | ZF=1 |
ZF位与SF位比较重要
五、test
作用:将两个数进行and操作,只改变标志寄存器的值。
格式:
TEST R/M,R/M/IMM #同样前面不能同时为操作数
常见用法:一般用来判断某寄存器值是否为0(一般看ZF标志寄存器判断)
六、jcc指令
1、je、jz
je:jmp if equal
jz:jmp if zero
有条件跳转,如果相等则跳转,一般看ZF标志位。
2、jne,jnz(与je、jz相反)
jne:jmp if not equal
jnz:jmp if not zero
3、js、jns
jmp if(not) sign
如果SF位为1(0)则跳转。
4、jp、jpe
jmp if parity (even)
PF=1跳转
5、jnp、jpo
jmp if not parity;jmp when parity flag is odd
PF=0跳转
6、jo、jno
jmp if (not) overflow
OF=1(0)则跳转
7、jb、jnae
jmp if below;jmp if above or equal
jb指令前面的cmp等比较指令中前面的数跟后面的数比较,小于则跳转(无符号数),即CF=1跳转。
8、jnb、jae
jmp if not below;jmp if above or equal
大于等于则跳转(无符号数),CF=0跳转
9、jbe、jna
jmp if below or equal;jmp if not above
小于等于则跳转(无符号数),CF=1或ZF=1跳转。
10、jnbe、ja
jmp if not below or equal;jmp if above
大于则跳转(无符号数),CF=0且ZF=0跳转。
11、jl、jnge
jmp if less;jmp if not greater or equal
小于则跳转(有符号数),SF≠OF跳转。
12、jnl、jge
jmp if not less;jmp if greater or equal
大于等于则跳转(有符号数),SF=OF跳转。
13、jle、jng
jmp if less or equal;jmp if not greater
小于等于则跳转(有符号数),SF≠OF或ZF=1。
14、jnle、jg
jmp if not less or equal;jmp if greater
大于则跳转(有符号数),SF=OF且ZF=0。
堆栈图
OllyDBG F2设置断点,F9运行到断点处,F8单步运行,CALL用F7执行。
画堆栈图
(初始堆栈)↑
push 2
和push 1
操作一般是将函数要用到的参数赋值并压入栈中,后面函数可能要使用;call
函数表示调用此函数,让CPU跳转到后面跟的地址,开始执行函数,先会经过jmp
指令,起一个跳板的作用,两个都会改变eip的值,让CPU跳转执行,而且最要注意的是,call
函数跳转前会先将函数的返回地址压入栈中;push ebp
是将函数调用前的源栈底储存后面,后面要恢复堆栈
-
mov ebp,esp
、sub esp,40
为了开辟缓冲区,用来存储函数中要用到的数据 -
push ebx
、push esi
、push edi
为了保留现场,这些寄存器中可能存着函数调用前的信息,函数执行时可能会使用这些寄存器,所以将数据先存入内存 -
lea edi, dword ptr [ebp-40]
将缓冲区开始地址放入edi -
mov ecx,10
、mov eax,cccccccc
、rep stos dword ptr es:[edi]
将缓冲区用cccccccc填满 -
CCCCCCCC是四个十六进制数,CC其实就是int3的硬编码,int3是断点,填充这么多CCCCCCCC有什么好处呢?防止缓冲区溢出:因为在写代码时,这块新分配出来的内存(缓冲区),就是给程序用的,当有数据要存储时,就会存到这块内存中,其他不用的地方全是CCCCCCCC,即int3,那么当程序一旦超过缓冲区时,就会自动停下来
-
其次,这块新分配出来的内存,上一个函数执行时可能使用过,留下垃圾数据(对本次函数没有意义的数据),那么将缓冲区填充的还有一个好处就是将缓冲区的垃圾信息覆盖,所以最好给参数赋初始值
mov eax, dword ptr [ebp+8]
、add eax, dword ptr [ebp+C]
这两条指令就是函数真正的功能,将两个函数的参数相加,将结果存到eax中返回(如果结果大于0xffffffff可以存到内存中)pop edi
、pop esi
、pop ebx
用来恢复现场,将原来的寄存器中值恢复retn
等价于pop eip,而此时栈顶中的值就是函数的返回地址,所以此指令执行后让CPU回到这函数的下面一行指令地址add esp,8
是为了将堆栈恢复原状
本文作者:yee-l
本文链接:https://www.cnblogs.com/yee-l/p/18232888
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步