C/C++对应的汇编语言
从汇编语言层面理解 i++ 与 ++i 的区别——以while循环为例
i++:
这里[rbp-0x4]对应 i ,之后把i的当前值0存到eax中,后面把当前值+1(rax + 0x1)存到edx,再覆盖原来的i (rbp - 0x4)的值,后面就是比较当前值与4的大小决定是否跳出循环,而不是+1后的值
5 int i = 0;
0x00007ff6137e145d <+13>: mov DWORD PTR [rbp-0x4],0x0
6
7
8 while (i++ < 5)
=> 0x00007ff6137e1464 <+20>: jmp 0x7ff6137e1478 <main()+40>
0x00007ff6137e1478 <+40>: mov eax,DWORD PTR [rbp-0x4]
0x00007ff6137e147b <+43>: lea edx,[rax+0x1]
0x00007ff6137e147e <+46>: mov DWORD PTR [rbp-0x4],edx
0x00007ff6137e1481 <+49>: cmp eax,0x4
0x00007ff6137e1484 <+52>: setle al
0x00007ff6137e1487 <+55>: test al,al
0x00007ff6137e1489 <+57>: jne 0x7ff6137e1466 <main()+22>
9 {
10 printf("���õ����� %d\n", i);
0x00007ff6137e1466 <+22>: mov edx,DWORD PTR [rbp-0x4]
0x00007ff6137e1469 <+25>: lea rax,[rip+0x2b90] # 0x7ff6137e4000
0x00007ff6137e1470 <+32>: mov rcx,rax
0x00007ff6137e1473 <+35>: call 0x7ff6137e2600 <printf>
因此对于i++而言,同一行代码内,程序使用的是当前值,而从下面一行代码开始,程序使用的是+1后的值。接下来我们来看++1的反汇编。
++i:
与上面不同的是,这里直接用add指令给i(rbp - 0x4)+1,省略了其中对eax、edx的操作,因此在同一行代码中,程序使用的是+1后的值。
13 i = 0;
0x00007ff653a8148b <+59>: mov DWORD PTR [rbp-0x4],0x0
14 while (++i < 5)
0x00007ff653a81492 <+66>: jmp 0x7ff653a814a6 <main()+86>
0x00007ff653a814a6 <+86>: add DWORD PTR [rbp-0x4],0x1
0x00007ff653a814aa <+90>: cmp DWORD PTR [rbp-0x4],0x4
0x00007ff653a814ae <+94>: setle al
0x00007ff653a814b1 <+97>: test al,al
0x00007ff653a814b3 <+99>: jne 0x7ff653a81494 <main()+68>
15 {
16 printf("ǰ�õ����� %d\n", i);
0x00007ff653a81494 <+68>: mov edx,DWORD PTR [rbp-0x4]
0x00007ff653a81497 <+71>: lea rax,[rip+0x2b71] # 0x7ff653a8400f
0x00007ff653a8149e <+78>: mov rcx,rax
0x00007ff653a814a1 <+81>: call 0x7ff653a82600 <printf>
表面上看++i比i++更好理解一些,而且运行起来比较简单不需要产生临时的副本,更关键的是++i只需要add指令,不需要像i++一样频繁访问内存,不仅更快而且更安全。
汇编部分
lea指令
加载有效地址到目标寄存器
lea dest,[addr]
lea指令通常用来实现算术运算
实现方法:
lea edx,[rax+0x1]
这里rax + 1的值被当成一个地址存储到了edx
优点:
--AI生成--
- 一步到位,不破坏原寄存器:
lea edx, [rax+0x1]可以实现三操作数指令的效果(类似edx = rax + 1),保留了rax中的原值(我们在后面的cmp eax, 0x4中还需要用到这个原值)。普通的add会覆盖目标寄存器。 - 不修改标志寄存器(EFLAGS):普通算术指令(如
add、sub)执行后会更新 CPU 的状态标志(如溢出标志、进位标志、零标志等),而lea纯粹是计算地址,完全不会触发任何状态标志的改变。这使得编译器进行指令调度时非常自由,不怕破坏上下文中其他条件跳转依赖的标志位。 - 计算能力强:
lea内部的寻址电路非常强大,一个时钟周期就能完成类似基址 + 变址 * 比例因子 + 偏移量的复杂算术运算(例如lea eax, [edi + ebx*4 + 8]),比多条连续的add/mul要快得多。
环绕特性:
如果lea指令溢出,比如lea edx,[eax + 1]此时eax = FFFFFFFF,加1得到0x100000000,此时lea会忽略掉1,然后把0存到edx。
and指令
给对应位置0
and al,10111111B ;给第6位置0
assume cs:code,ds:data
data segment
db 'linux'
db 'foRK'
data ends
code segment
start:
mov ax,data
mov ds,ax
mov bx,0
mov cx,9
s: mov al,[bx]
and al,11011111B ;如果变小写,则用or指令:or al,00100000B
mov [bx],al
inc bx
loop s
mov ax,4C00H
int 21H
code ends
end start
or指令
给对应位置1
or al,01000000B ;给第6位置1
源码:
#include <stdio.h>
int main()
{
int i = 0;
while (i++ < 5)
{
printf("后置递增: %d\n", i);
}
i = 0;
while (++i < 5)
{
printf("前置递增: %d\n", i);
}
return 0;
}
mul指令
引用书中文字叙述

调用new
int *p = new int;
0x00007ff63696149d <+13>: mov ecx,0x4
0x00007ff6369614a2 <+18>: call 0x7ff636961500 <_Znwy>
0x00007ff6369614a7 <+23>: mov QWORD PTR [rbp-0x10],rax
- int占4字节内存,把4放到rcx作为new的参数
- 调用operator new,这里的_Znwy是new的被修饰名
- new返回的指针地址保存在rax,把这个地址存到p所在的内存空间
rbp - 0x10
调用delete
delete p;
0x00007ff6369614d4 <+68>: mov rax,QWORD PTR [rbp-0x10]
0x00007ff6369614d8 <+72>: test rax,rax
0x00007ff6369614db <+75>: je 0x7ff6369614ea <main()+90>
0x00007ff6369614dd <+77>: mov edx,0x4
0x00007ff6369614e2 <+82>: mov rcx,rax
0x00007ff6369614e5 <+85>: call 0x7ff636961508 <_ZdlPvy>
- 用rax保存指针p
- 测试是否为空指针,如果是就不会释放内存
- int的大小作为delete第二个参数存入rdx
- 指针p被放到rcx作为第一个参数
- 调用operator delete
return
return 0;
0x00007ff6369614ea <+90>: mov eax,0x0
- rax保存返回值0
"{" 与 "}"
{
0x00007ff636961490 <+0>: push rbp
0x00007ff636961491 <+1>: mov rbp,rsp
0x00007ff636961494 <+4>: sub rsp,0x30
0x00007ff636961498 <+8>: call 0x7ff6369615f0 <__main>
}
0x00007ff6369614ef <+95>: add rsp,0x30
0x00007ff6369614f3 <+99>: pop rbp
0x00007ff6369614f4 <+100>: ret
{
- 把旧指针保存到栈上
- 把rbp换成新指针,所以后边就用rbp+/rbp-访问局部变量或参数
- 在栈上分配0x30大小的空间,用于局部变量,栈溢出,对齐等
- 初始化
}
- 恢复rsp
- 恢复调用者的帧指针
- 返回到调用点
Example

纪念第一次成功运行
assume cs:code,ds:a,es:b,ss:c
a segment
db 1,2,3,4,5,6,7,8
a ends
b segment
db 1,2,3,4,5,6,7,8
b ends
c segment
db 0,0,0,0,0,0,0,0
c ends
code segment
start:
mov ax,a
mov ds,ax
mov ax,b
mov es,ax
mov ax,c
mov ss,ax
mov sp,10h ;把c当stack,定义了8个byte,实际分配了16个字节,应为10h
mov ax,0
mov bx,0
mov cx,8
mov si,0
s: mov al,ds:[si] ;si作为索引寄存器,ai说dx不能直接做内存索引
mov bl,es:[si]
add al,bl
push ax
add si,1
loop s
mov ax,4C00H
int 21H
code ends
end start
第二个

assume cs:code,ds:a,ss:b
a segment
dw 1,2,3,4,5,6,7,8,9,0ah,0bh,0ch,0dh,0eh,0fh,0ffh
a ends
b segment
dw 0,0,0,0,0,0,0,0
b ends
code segment
start:
mov ax,a
mov ds,ax
mov ax,b
mov ss,ax
mov sp,10h
mov bx,0
mov cx,8
s: mov ax,ds:[bx]
push ax
add bx,2
loop s
mov ax,4C00H
int 21H
code ends
end start
call & ret
乘方函数编写:
assume cs:code ss:stack
data segment
dw 1,2,3,4,5,6,7,8
dd 0,0,0,0,0,0,0,0
data ends
stack segment
dw 8 dup(0)
stack ends
code segment
start: mov ax,data
mov ds,ax
mov ax,stack
mov ss,ax
mov si,0 ;si指向第一组word单元
mov di,16 ;di指向第二组word单元
mov cx,8
s: push cx
mov bx,[si]
mov cx,3
call cube
mov [di],ax ;ax存储低位字节
mov [di].2,dx ;dx存储高位字节 小端序存储,先存低位再存高位
pop cx
add si,2
add di,4
loop s
mov ax,4C00H
int 21H
cube: mov ax,bx
sub cx,1
s0: mul bx
loop s0
ret
code ends
end start
注意事项
用于保护的push和pop一定要写在循环体内部,写在外部会导致第二次循环cx没有被保护
小端序存储:整数0x1234在内存里是这样的34 12,应当反着存
实验10 编写子程序
1Welcome to masm!
assume cs:code,ds:data,ss:stack
data segment
db 'Welcome to masm!',0
data ends
stack segment
dw 8 dup(0)
stack ends
code segment
start: mov ax,data
mov ds,ax
mov ax,stack
mov ss,ax
mov dh,8
mov dl,3
mov cl,2
call show_str
mov ax,4C00H
int 21H
;raw: dh * 160 col: 2 * (dl - 1)
show_str:
mov ax,0B800H
mov es,ax
mov si,0
mov ax,0
;raw
mov al,dh
mov bl,160
mul bl
mov bx,ax
;col
mov ax,0
mov al,dl
sub ax,1
add ax,ax
mov di,ax
show: mov ax,[si]
cmp ax,0
je ok
mov es:[bx + di],ax
mov es:[bx + di + 1],cl
add di,2
inc si
jmp short show
ok: ret
code ends
end start
;我真棒
show_str编写
- 参数:dh->行号 dl->列号 cl->颜色 ds:si->字符串首地址
- es存储显存段地址
- ax和bx用于临时存储计算数据
- bx + di用于定位显存位置
难点
- 定位显存位置:
raw: dh * 160 col: 2 * (dl - 1)。字符在显存地址空间中是按照低地址存ASCII码,高地址存属性方式存储的。一个字符占2字节,一行80个一共160个字节,dh * 160定位每行起始地址。00-01存储第一个字符,02-03存储第二个字符,04-05存储第三个字符,据此可以总结如下规律:每行第dl个字符(每列)起始地址是2 * (dl - 1)。 - 参数占用寄存器:理想情况下,raw计算乘法需要ax、bx寄存器,保存结果用si,同样的col计算也需要ax,bx,还需要di保存结果。但是已经si被用来定位字符串,所以不能用这个寄存器。而从哪里可以省出一个寄存器呢?答案是计算col的时候,
col: 2 * (dl - 1)由于乘数2本身比较小,可以直接把mul指令替代为add指令,这样就可以省出一个寄存器bx(如果数更大的话,可能需要用栈传递参数)。然后就可以用bx + di定位显存。 - show:跟教材上边jcxz不一样的是这里用je跳转,同样是因为寄存器被占用的问题。
最终效果

divdw编写
assume cs:code,ss:stack
stack segment
db 8 dup(8)
db 8 dup(8)
stack ends
code segment
start: mov ax,stack
mov ss,ax
mov sp,10h
mov dx,21h
mov ax,0e88eh
mov cx,6h
call divdw
mov ax,4c00h
int 21h
divdw: push ax ;将被除数低16位入栈
;如果不想用栈,那么就需要在四个寄存器中来回交换
;先计算H/N,除数为16位,被除数为32位
mov ax,dx ;(ax)=(dx)=被除数高16位
xor dx,dx ;被除数高16位位0
div cx ;(ax)=商,(dx)=余数
;int(H/N)*65536
mov bx,ax ;暂存结果高16位
;由于65536的十六进制是10000,所以做乘法的时候ax永远是0,
;(dx)=1=商*1=(ax)
;同理,[rem(H/N)*65536+L]/N 等价于 (H%N)*10000+L
;由于10000需要两个寄存器来存储(ax)=0000,(dx)=0001
;所以(H%N)*10000在寄存器中的值为(ax)=0000,(dx)=H%N*1
;同时L的取值范围[0,ffff]
;所以,(H%N*65536+L])寄存器的值为(dx)=H%N*1,(ax)=L
;(dx)=余数
pop ax ;弹出除数低16位
div cx ;[rem(H/N)*65536+L]/N
mov cx,dx ;将余数送入cx
mov dx,bx ;将结果高16位送入dx
ret
code ends
end start
作者:摸鱼校尉002
链接:https://juejin.cn/post/7128724324319494157
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
dtoc将word型数据转换为十进制字符串函数编写
;arg:ax 要转换的word型数据
dtoc: mov bx,10
s: mov dx,0
div bx
add dx,30H
push dx
inc si
cmp ax,0
je dtoc_ok
jmp short s
dtoc_ok:mov cx,si
mov di,0
s_ok:
pop [di]
inc di
loop s_ok
ret
最终效果

圣经

解释:大写字母范围41H ~ 5AH,对应二进制是 100 0001 ~ 101 1010。此外,后边还有几个符号,算上符号后范围到达了110 0000。这时候,第五位才是1,自然后边小写字母第五位都是1了。这样一来,大写字母第五位都是0,小写字母则都是1,所以运用上文and与or指令的性质可以自由地转换大小写字母。
另一种分析思路
大写字母与小写字母之间差了20H,而且它的二进制正好是10 0000,所以他们第五位是相异的,即大写字母是0,小写字母是1,然后运用and与or指令性质转换一下就可以了
(具体代码详见and or示例)
浙公网安备 33010602011771号