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++一样频繁访问内存,不仅更快而且更安全。

汇编部分

关于有符号数

例:视FC为有符号数,对应二进制表示是 11111100,最高有效位(最坐标的位)是1,所有FC是负数,将所有位取反并加1,得到 100,所以FC表示-4

Flag标志位

CF:发生进位/借位则置1 ==> 无符号数
OF:溢出标志位 ==> 有符号数

mov al,0FCH
add al,05H

执行完后 CF = 1,OF = 0
这是因为对于无符号数,FC + 5 产生进位,故CF被置1;对于有符号数,FC表示-4,-4 + 5 = 1,1在-128 ~ 127之间,所以不会产生溢出

mov al,7DH
add al,0BH

执行后 CF = 0,OF = 1
如果视为无符号数,7D + B = 88,没有产生进位,所以CF = 0;如果视为有符号数,7D表示+125,B表示+11,加起来超过127,产生溢出,OF = 1

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

编写0号中断处理程序

assume cs:code



code segment 
start:  
        ;Setup do0
        mov ax,cs
        mov ds,ax
        mov si,offset do0;src
        mov ax,0
        mov es,ax   
        mov di,200h     ;dest
        mov cx,offset do0end-offset do0
        cld
        rep movsb

        ;Set table

        mov ax,0
        mov es,ax
        mov word ptr es:[0*4],200h
        mov word ptr es:[0*4+2],0

        ;exception
        mov ax, 1000h
        mov bh, 1
        div bh
        ;屏幕中间出现overflow!字样

        mov ax,4C00H
        int 21H


do0:    ;show overflow!
        jmp short do0start
        db "overflow!"
do0start:
        mov ax,cs
        mov ds,ax
        mov si,202h

        mov ax,0B800h
        mov es,ax
        mov di,12 * 160 + 36 * 2

        mov cx,9
    s:  mov al,[si]
        mov es:[di],al
        inc si
        add di,2
        loop s

        mov ax,4C00H
        int 21H
do0end: nop
code ends
end start

原理说明

当程序产生中断消息的时候,CPU根据中断类型码在中断向量表中找到相应中断处理程序的入口地址。这意味着,只要修改对应中断处理程序的入口地址,就可以重定向到我们自己写的中断处理程序。

在8086PC机中,内存地址0000:0000~0000:03FF中1024个字节存放中断向量表,且入口地址是按照低地址存放偏移地址,高地址存放段地址。既然知道中断向量表的位置了,那异常处理程序应该放在哪里呢。由于CPU随时都可能会收到中断消息,也就是随时都有可能调用中断处理程序,所以中断处理程序必须一直存储在一段内存空间中。根据书中内容,0000:0200~0000:02FF是空闲区段,所以只需要把我们的程序搬到这里即可。

如何编写异常处理程序?
功能:在发生除法错误的时候,显示overflow!
还有一个问题没解决,CPU怎么知道overflow的位置在哪?答案是我们需要在do0内部存储该字符串,然后发生中断时CPU会根据中断向量表将CS:IP会定位到异常处理程序,此时就能通过CS定位overflow了。

具体方法和show_str一样,只不过因为字符串不是可执行代码,开头需要增加一个占两个字节的jmp指令,这时候字符串的位置就可以这样处理。

mov ax,cs
mov ds,ax
mov si,202h

运行效果

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%  阅读(14)  评论(0)    收藏  举报