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生成--

  1. 一步到位,不破坏原寄存器lea edx, [rax+0x1] 可以实现三操作数指令的效果(类似 edx = rax + 1),保留了 rax 中的原值(我们在后面的 cmp eax, 0x4 中还需要用到这个原值)。普通的 add 会覆盖目标寄存器。
  2. 不修改标志寄存器(EFLAGS):普通算术指令(如 addsub)执行后会更新 CPU 的状态标志(如溢出标志、进位标志、零标志等),而 lea 纯粹是计算地址,完全不会触发任何状态标志的改变。这使得编译器进行指令调度时非常自由,不怕破坏上下文中其他条件跳转依赖的标志位。
  3. 计算能力强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指令

引用书中文字叙述
image

调用new

int *p = new int;
0x00007ff63696149d <+13>:	mov    ecx,0x4
0x00007ff6369614a2 <+18>:	call   0x7ff636961500 <_Znwy>
0x00007ff6369614a7 <+23>:	mov    QWORD PTR [rbp-0x10],rax
  1. int占4字节内存,把4放到rcx作为new的参数
  2. 调用operator new,这里的_Znwy是new的被修饰名
  3. 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>
  1. 用rax保存指针p
  2. 测试是否为空指针,如果是就不会释放内存
  3. int的大小作为delete第二个参数存入rdx
  4. 指针p被放到rcx作为第一个参数
  5. 调用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

{

  1. 把旧指针保存到栈上
  2. 把rbp换成新指针,所以后边就用rbp+/rbp-访问局部变量或参数
  3. 在栈上分配0x30大小的空间,用于局部变量,栈溢出,对齐等
  4. 初始化
    }
  • 恢复rsp
  • 恢复调用者的帧指针
  • 返回到调用点

Example

image
纪念第一次成功运行

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

第二个
image

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

注意事项

用于保护的pushpop一定要写在循环体内部,写在外部会导致第二次循环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编写

  1. 参数:dh->行号 dl->列号 cl->颜色 ds:si->字符串首地址
  2. es存储显存段地址
  3. ax和bx用于临时存储计算数据
  4. 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跳转,同样是因为寄存器被占用的问题。

最终效果

image

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

最终效果

image

圣经

image

解释:大写字母范围41H ~ 5AH,对应二进制是 100 0001 ~ 101 1010。此外,后边还有几个符号,算上符号后范围到达了110 0000。这时候,第五位才是1,自然后边小写字母第五位都是1了。这样一来,大写字母第五位都是0,小写字母则都是1,所以运用上文and与or指令的性质可以自由地转换大小写字母。

另一种分析思路
大写字母与小写字母之间差了20H,而且它的二进制正好是10 0000,所以他们第五位是相异的,即大写字母是0,小写字母是1,然后运用and与or指令性质转换一下就可以了
(具体代码详见and or示例)

posted on 2026-05-07 13:35  %HuTao%  阅读(3)  评论(0)    收藏  举报