C++反汇编语言 – 入门

  • A+
所属分类:逆向工程

# 一、简介

# 二、基础

#   1. 定义数据

  • C++:在 C++ 中可以用 bool、short、int、float、char、double 等基础类型定义数据,那么它们在汇编语言中是如何表现的呢?
  • 汇编语言定义数据
    • 单位:byte(1 字节)、word(1 字、2 字节)、double word(双字、4 字节)、quad word(4 字、8 字节)。
    • 定义数据(伪指令):
      • db:define byte,定义操作数占用 1 字节大小。
      • DW 定义操作数占用两字节大小,DD 定义操作数占用四个字节、DQ 定义操作数占用八个字节。
      • 例如:
        • dw 0123h, 0456h, 0789h, 0abch -> 定义了 4 个数据,每个数据占用 2 个字节,一共占用 8 个字节。
        • db 'Hello' -> 字符串应该拆开来看,所以定义 hello 每个字符占用 1 字节大小,一共占用 5 字节大小。
  • C++ 定义的基本数据类型 =》反汇编 =》汇编语言申明内存大小
    • 用 byte、word、dword、qword 等,放在地址前用来定义操作内存的大小。
    • 8 位 / 16 位 / 32 位 / 64 位的寄存器,代表不同大小。注:32 位寄存器多用 E 开头,64 位寄存器多用 R 开头。
    • 例子:
          bool b = true;
              mov         byte ptr [rbp+4],1          \\\ rbp存放的是栈底地址,所以这里在栈上分配了一个字节大小空间[rbp+4]到[rbp+5],存储bool类型的值,并赋初值。
          int i = 1;
              mov         dword ptr [rbp+24h],1       \\\ 同理,注意有h的代表十六进制数,没有h代表十进制数!
          float f = 1.2;
              movss       xmm0,dword ptr [00007FF75A089BB0h]  \\\ xmm0是浮点数的寄存器,浮点数的计算方法比较特殊。
              movss       dword ptr [rbp+44h],xmm0    \\\ 先将浮点数移入xmm0中,然后在放入栈上(rbp+44h - rbp+4ch)。
          char c = 'a';
              mov         byte ptr [rbp+64h],61h      \\\ 字符串都会翻译成二进制编码(如:ASCII、UTF-8、GB2312等),存储在内存中!
      
  • 总结
    • C++ 是强类型数据语言,所有的数据都必须定义类型,类型用于在 C++ 编译器中解释该类型的数据大小!从而将 C++ 数据转为汇编语言的数据。
      • .cpp -> C++ 编译器 -> 汇编语言 -> 汇编编译器 -> 二进制语言
      • C++ 的数据类型更符合人类的认知。
    • 汇编语言数据的定义更符合计算机的工作。对于汇编语言来讲,数据都是二进制类型(计算机只能存储二进制数据),所以只关心数据长度或大小(类型相同:二进制 0 和 1)!
      至于数据怎么使用(相加、相减、字符合并,输出等等)都交给程序员决定,有点弱数据类型语言的味道!

#   2. 汇编语言 - 段(伪指令)

  • 汇编语言有数据和代码,数据又可以根据需求不同继续细分(栈数据、堆数据、全局数据...),代码又可以根据功能不同继续细分,
    那么汇编语言如何解决程序越来越复杂时,将不同的数据、代码拆分开来!
  • Segment (段),顾名思义就是将数据、程序根据需求一段一段的拆分出来!
  • 定义段:
    • 用伪指令 assume 定义段,后面直接跟段名。
    • 定义三个段,分别为代码段、数据段、栈段:assume cs:code, ds:data, ss:stack,var
      • cs:ip 是代码指针寄存器,它们指向何处就该何处的代码执行。cs 是段寄存器,RIP 是偏移地址寄存器(段 + 偏移的逻辑地址算法)。
      • ds:bx,ss:sp 分别是数据段和栈段指针寄存器,它们指向何处何处就是数据段和栈空间。
      • assume cs:code 表示定义段 code,并将 cs:ip 指针指向段开始的地址。当然也可以像 var 那样,只定义段。
  • 使用段 =》8086 机汇编代码示例:
    ;代码功能:将data段中的数据依次入栈到stack栈段,然后stack栈段又依次出栈到data。

    assume cs:code, ds:data, ss:stack
    data segment                ;段开始
        dw 0123h, 0456h, 0789h, 0abch, 0123h, 0456h, 0789h, 0abch
    data ends                   ;段结束

    stack segment
        dw 16 dup (0)           ;用dup定义16个dw数据,初始化为0
    stack ends

    code segment
    start: mov ax,stack         ;将stack段首地址放入ax中
           mov ss,ax            ;用ss指向stack,其实就是上面的SS:stack,只是这里演示用程序动态指向stack。
           mov sp,20h           ;设置栈顶ss:sp指向stack:20
           
           mov ax,data
           mov ds,ax            ;同上,ds指向data段

           mov bx,0             ;ds:bx指向data段中的第一个单元

           mov cx,8             ;cx是循环计数器
    s:     push [bx]
           add bx,2
           loop s               ;将data段中的0~15单元中的数据依次入栈

           mov bx,0
           
           mov cx,8
   s0:    pop [bx]
           add bx,2
           loop s0              ;以此出栈数据到data段中的0~15单元中

           mov ax,4c00h         ;返回上一级的代码
           int 21h
    code ends                   ; code段结束
    end start                   ; start结束



;注:start就是一个标号,标志程序的入口而已,程序加载到内存之后CS:IP会指向这个标号,从start指向的指令开始运行
;这个标号不一定是start,你也可以用main,但在程序的最后要用end main来提示程序结束
;start也不一定在代码段的最前面,它的前面是可以有指令或数据的。
  • 常见的数据段
    • BSS 段: BSS 段。用来存放程序中未初始化或初始化为 0 的全局变量的一块内存区域。BSS 是英文 BlockStarted by Symbol 的简称,BSS 段属于静态内存分配。
    • DATA 段:数据段。用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。
    • TEXT 段:代码段。用来存放程序执行代码的一块内存区域,只读属性。
    • CONST 段:常量段。用来存放程序中常量数据的一块内存区域,只读属性。

#   3. VS 反汇编

  • VS 的调试窗口下,有寄存器、内存、汇编代码、Watch 等窗口可以查看跟踪程序汇编代码。
  • 输出汇编代码:项目 -> 属性 ->C/C++-> 输出文件 -> 汇编程序输出 -> 程序集、机器码和源码
    • 输出文件 /x64/Debug/YourCppName.cod 或者 YourCppName.asm

# 三、常量和变量

#   1. 常量和变量简述

  • C++:栈区、堆区、全局区、常量区、代码区等
  • 汇编语言:常量段、数据段、栈段、堆段、代码段等
  • 二进制语言:内存地址 + 属性
    • 属性:可读、可写、可执行
    • 变量(可读写)、常量(可读)、代码(可执行)

#   2. 宏

  • 宏会被 C++ 编译器直接替换目标代码!
    #define TEST_MACRO_NUM 10
    #define TEST_MACRO_Add(a,b) a+b
    #define TEST_MACRO_SQRT(c) sqrt(c)

    int main(int argc, char* argv[])
    {
        /// ...
	        printf("%d\n", TEST_MACRO_NUM);
        mov         edx,0Ah                    /// TEST_MACRO_NUM被直接替换成立即数10=0Ah
        lea         rcx,[string "%d\n"]
        call        printf                     /// 调用Print函数
	        printf("%d\n", TEST_MACRO_Add(1, 2));
        mov         edx,3                      /// 编译器优化1+2=3(能直接计算或能简化都会被编译器优化)
        lea         rcx,[string "%d\n"]  
        call        printf  
	        printf("%d\n", TEST_MACRO_SQRT(3));
        mov         ecx,3                          /// 将参数3传给寄存器ecx
        call        sqrt<int,0>                    /// 调用sqrt函数,参数为ecx /// sqrt模板类根据类型展开
        movaps      xmm1,xmm0  
        movq        rdx,xmm1  
        lea         rcx,[string "%d\n"]  
        call        printf 
	        return 0;
        xor         eax,eax 
    }

#   3. const 常量

  • 在所有直接使用 const 的常量的地方,C++ 编译器会在编译期间直接替换成常量或立即数。
  • 但实质还是个变量,占用内存,可以被强制修改其内存的值!
    int main(int argc, char* argv[])
    {
        /// ...
    	        const int iVar = 1;
        mov         dword ptr [rbp+4],1  /// 在栈上分配了内存,首地址是[rbp+4],大小为dword
	        printf("%d\n", iVar);
        mov         edx,1              /// 直接替换成立即数
        lea         rcx,[string "%d\n"]  
        call        printf  

	        int* pVar = (int*)&iVar;            /// 强转去掉const属性,然后将用指针指向const变量的内存
        lea         rax,[rbp+4]  
        mov         qword ptr [rbp+28h],rax  
	        ++(*pVar);                          /// +1
        mov         rax,qword ptr [rbp+28h]  
        mov         eax,dword ptr [rax]  
        inc         eax  
        mov         rcx,qword ptr [rbp+28h]  
        mov         dword ptr [rcx],eax  
	        printf("%d\n", iVar);
        mov         edx,1              /// 直接替换成立即数
        lea         rcx,[string "%d\n"] 
        call        printf  
	        printf("%d\n", *pVar);              /// 输出const变量内存的值
        mov         rax,qword ptr [rbp+28h]  
        mov         edx,dword ptr [rax]  
        lea         rcx,[string "%d\n"]  
        call        printf  
	        return 0;
        xor         eax,eax 
    }

输出结果:1、1、2
  • #define 和 const 常量
    • #define 真常量,编译期间就会被替换,宏符号名称不会出现在执行文件中!
    • const “假” 常量,只是编译期间对语法进行了检查,并在编译期间直接替换使用 const 常量的地方,但实质还是个变量。
    • 都是 C++ 语法,汇编语言(只有 CONST 段及只读区域)和二进制编码中没有这两种类型的存在。

#   3. 常量总结

  • 常量基础
    • 字面意思:常量是一个恒定不变的值,它在内存中是不可被修改的!
    • 程序中的 1、2、3 等数字或者 "Hello World!" 这样的字符,以及数组名称都属于常量,它们在运行时不能被修改。
    • 常量在程序运行前就已经存在,它们被编译进可执行文件中!
      • 立即数常量被直接写进代码中。
      • 常量数据区,该区域程序没有写的权限。
      • 代码区,如数组名称,this,函数名和函数参数名等等。(看汇编代码像是这样,仅表示个人看法)
  • 常量测试
    • C++ 代码
      int main(int argc, char* argv[])
      {
          printf("Hello Wolrd!\n");
          string s = "I am string";
          int intArray[3] = { 1,2,3 };
      
          const int iVar = 1;
          printf("%d\n", iVar);
      #define IVAR 2
          printf("%d\n", iVar);
      
          return 0;
      }
      
      • 汇编代码(VS 生成的.asm 文件)
      /// 汇编语言中";"表示后面是注释
      //////////////////////////// 常量数据区(CONST 段) ////////////////////////////
      
      /// print中的 %d 字符
      CONST	SEGMENT
      ??_C@_03PMGGPEJJ@?$CFd?6@ DB '%d', 0aH, 00H		; `string'
      CONST	ENDS
      
      /// 字符 I am string
      CONST	SEGMENT
      ??_C@_0M@CFPJMFMA@I?5am?5string@ DB 'I am string', 00H	; `string'
      CONST	ENDS
      
      /// 字符 Hello Wolrd!
      CONST	SEGMENT
      ??_C@_0O@DDOOOAMO@Hello?5Wolrd?$CB?6@ DB 'Hello Wolrd!', 0aH, 00H ; `string'
      CONST	ENDS
      
      //////////////////////////// 代码区 ////////////////////////////
      /// 数组名字,intArray$ = 72,表示偏移72个内存单元
      /// 例如:intArray$[rbp],表示距离栈底地址[rbp]偏移72,运行时的汇编代码为[rbp+72] = [rbp+48h]
      /// 所以运行时intArray的首地址为[rbp+48h]
      intArray$ = 72
      
      
      //////////////////////////// Main函数C++代码和对应的汇编代码 ////////////////////////////
      main	PROC						; COMDAT
      
      ; 6    : {
      ; 7    : 	printf("Hello Wolrd!\n");
         
          /// 取出字符地址,将地址赋值给rcx寄存器传参,调用print函数
          lea	rcx, OFFSET FLAT:??_C@_0O@DDOOOAMO@Hello?5Wolrd?$CB?6@
          call	printf
      
      ; 8    : 	string s = "I am string";
      
          lea	rdx, OFFSET FLAT:??_C@_0M@CFPJMFMA@I?5am?5string@
          lea	rcx, QWORD PTR s$[rbp]
          ; std::basic_string<char,std::char_traits<char>,std::allocator<char> >::basic_string<char,std::char_traits<char>,std::allocator<char> >
          call	??0?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@QEAA@QEBD@Z
      
      ; 9    : 	int intArray[3] = { 1,2,3 };
          /// 数组相对rbp偏移
          mov	DWORD PTR intArray$[rbp], 1
          mov	DWORD PTR intArray$[rbp+4], 2
          mov	DWORD PTR intArray$[rbp+8], 3
      
      ; 11   : 	const int iVar = 1;
      
          mov	DWORD PTR iVar$[rbp], 1
      
      ; 12   : 	printf("%d\n", iVar);
          /// 常量直接编译成立即数
          mov	edx, 1
          lea	rcx, OFFSET FLAT:??_C@_03PMGGPEJJ@?$CFd?6@
          call	printf
      
      ; 13   : #define IVAR 2
      ; 14   : 	printf("%d\n", iVar);
      
          mov	edx, 1
          lea	rcx, OFFSET FLAT:??_C@_03PMGGPEJJ@?$CFd?6@
          call	printf
      
      	...
      ; 17   : }
      

#   4. 变量基础

  • 变量:可以变化的量,是可读可写的内存!
  • 变量有作用域和生命周期
    • 作用域指在源码中可以被访问到的范围。
    • 生命周期是变量所在的内存从分配到释放的时间。

#   4. 全局变量

  • 全局变量和常量类似,被写入可执行文件中,生命周期起源于执行文件被系统加载后,执行第一条代码前,这个时候已经具有内存地址了。
    程序结束运行并退出后,全局变量将被销毁。因此全局变量可以在程序的任何位置使用!
  • 代码示例:
    int g_iVar = 0;
    
    int main( )
    {
        ... 
    	        printf("%d", g_iVar );
        /// 全局变量g_iVar的地址07FF73D7EC170h属于全局数据区
        mov         edx,dword ptr [g_iVar (07FF73D7EC170h)]  
        lea         rcx,[string "%d" (07FF73D7E9C24h)]  
        call        printf (07FF73D7E1190h)  
              return 0;
        xor         eax,eax  
    }
    
    ////////////////////////////////// asm文件 //////////////////////////////////
    /// 全局变量g_iVar定义如下
    /// PUBLIC表示本模块和外部模块都可以访问变量g_iVar
    /// 因为g_iVar初始化为0,所以在段BSS中。
    PUBLIC	?g_iVar@@3HA					; g_iVar
    _BSS	SEGMENT
    ?g_iVar@@3HA DD	01H DUP (?)				; g_iVar
    _BSS	ENDS
    
    
    /// main函数汇编程序
    main	PROC						; COMDAT
    ; 8    : {
    ...
    
    ; 9    : 	printf("%d", g_iVar );
    
    	mov	edx, DWORD PTR ?g_iVar@@3HA		; g_iVar
    	lea	rcx, OFFSET FLAT:??_C@_02DPKJAMEF@?$CFd@
    	call	printf
    
    ; 10   : 	return 0;
    
    	xor	eax, eax
    
    ; 11   : }
    

#   5. 局部变量

  • 局部变量作用域在 “{}” 中,生命周期起始于程序执行变量定义(内存分配),终止于 “}”(内存释放)。
  • 局部变量作用于函数体内,在栈上分配内存。
    int main( )
    {
        ...
    	        bool bVar = 1;
        /// rbp是栈基底指针,为局部变量bVar在栈上分配内存[rbp+4,rbp+8],并赋初值1。
        mov         byte ptr [rbp+4],1  
    	        printf("%d", bVar );
        /// 将bVar作为第二个参数传递给print
        movzx       eax,byte ptr [rbp+4]  
        mov         edx,eax  
        /// 取第一个参数%d常量的内存地址,并赋值传参
        lea         rcx,[string "%d" (00007FF6216A9C24h)]  
        /// 调用print函数,这里是print函数的首地址
        call        printf (00007FF6216A1190)
    	        return 0;
        xor         eax,eax  
    }
        /// 释放局部变量!由栈平衡释放局部变量,栈寄存器rbp+rsp还原到调用者保存的栈寄存器数据。
        /// 程序跳出该函数,将执行权返回给调用者!
        /// 具体原理函数章节会细讲!
        lea	rsp, QWORD PTR [rbp+200]
        pop	rdi
        pop	rbp
        ret	0
    

#   6. 静态变量

  • 全局静态变量
    • 全局静态变量和全局变量类似,只是全局静态变量只能在当前文件使用,但这只是 C++ 编译器语法检查对其作出的限制。
    • 生命周期和全局变量一致。作用域在 C++ 中仅限当前文件使用,但在汇编语言中和全局变量一致。
    • 所以全局静态变量等价于 C++ 编译器限制外部文件访问的全局变量。
  • 局部静态变量
    • 从下面的汇编代码中可知,局部静态变量和全局静态变量的定义和使用完全一致,那么怎么限制局部静态只作用域函数体内 “{}”?
    • 估计和全局静态变量一样,是 C++ 编译器对局部静态变量的语法作出的限制
  • 代码示例:
    static int g_iVar = 1;
    int main( )
    {
        ... 
    	        printf( "%d\n", g_iVar );
        mov         edx,dword ptr [g_iVar (07FF77B4EC000h)]  
        lea         rcx,[string "%d" (07FF77B4E9C24h)]  
        call        main (07FF77B4E1190h)  
        
        /// 问题1:这里竟然没有局部静态变量赋初值的汇编代码???答案请见asm文件。
        /// 问题2:众所周知,C++局部静态变量赋初值的代码只执行一次,那么汇编语言是怎么实现的?
    	        static int iVar = 2;
    	        printf( "%d\n", iVar );
        /// 这三句全是printf传参和调用的代码
        mov         edx,dword ptr [iVar (07FF77B4EC004h)]  
        lea         rcx,[string "%d" (07FF77B4E9C24h)]  
        call        printf (07FF77B4E1190h) 
        
    	        iVar++;
        /// 对比上面的g_iVar内存地址,可知局部静态变量iVar也在全局数据区
        mov         eax,dword ptr [iVar (07FF77B4EC004h)]  
        inc         eax  
        mov         dword ptr [iVar (07FF77B4EC004h)],eax 
        
    	        printf( "%d\n", iVar );
        mov         edx,dword ptr [iVar (07FF77B4EC004h)]  
        lea         rcx,[string "%d" (07FF77B4E9C24h)]  
        call        printf (07FF77B4E1190h)  
    	        return 0;
        xor         eax,eax  
    }
    
    ////////////////////////////////// asm文件 //////////////////////////////////
    /// 全局静态变量和局部静态变量都定义在DATA数据段中。
    /// 问题1-答案:因为局部静态变量的初值被写入了文件,和全局变量一样在可执行文件被系统加载后,第一段代码执行前就已经被赋了初值,所以没有也不需要在函数中赋初值的汇编代码!
    /// 问题2-答案:因为初值早就有了,函数体内也没有赋初值的汇编代码,所以也就实现了C++所谓的局部静态变量赋初值代码只会执行一次的功能!
    _DATA	SEGMENT
    ?g_iVar@@3HA DD	01H					; g_iVar
    ?iVar@?1??main@@9@4HA DD 02H				; `main'::`2'::iVar
    _DATA	ENDS
    
    
    /// Main函数代码
    _TEXT	SEGMENT
    main	PROC						; COMDAT
    
    ; 7    : {
            ...
    ; 8    : 	printf( "%d\n", g_iVar );
    
    	mov	edx, DWORD PTR ?g_iVar@@3HA
    	lea	rcx, OFFSET FLAT:??_C@_03PMGGPEJJ@?$CFd?6@
    	call	printf
    
    ; 10   : 	static int iVar = 2;
    ; 11   : 	printf( "%d\n", iVar );
    
    	mov	edx, DWORD PTR ?iVar@?1??main@@9@4HA
    	lea	rcx, OFFSET FLAT:??_C@_03PMGGPEJJ@?$CFd?6@
    	call	printf
    
    ; 12   : 	iVar++;
    
    	mov	eax, DWORD PTR ?iVar@?1??main@@9@4HA
    	inc	eax
    	mov	DWORD PTR ?iVar@?1??main@@9@4HA, eax
    
    ; 13   : 	printf( "%d\n", iVar );
    
    	mov	edx, DWORD PTR ?iVar@?1??main@@9@4HA
    	lea	rcx, OFFSET FLAT:??_C@_03PMGGPEJJ@?$CFd?6@
    	call	printf
    
    ; 14   : 	return 0;
    
    	xor	eax, eax
    
    ; 15   : }
        ...
    main	ENDP
    _TEXT	ENDS
    

#   7. 成员变量

  • 面对对象 =》对象 = 成员变量 + 函数 = 属性 + 方法
  • 成员变量的生命周期为:对象的内存分配 到 对象的销毁
    • new 的堆对象生命周期:new 到 delete
    • 栈对象:对象栈内存分配 到 “}”
  • 成员变量的作用域:public、protect、private(面对对象三大特性之封装)
  • 成员变量的读写:对象首地址 + 偏移地址(段地址 + 偏移地址)
  • 代码示例
    class People
    {
    public:
      int m_iAge;
      bool m_bIsWoman;
    };
    
    int main( )
    {
          ... 
              People* pPepole = new People();
       /// ...分配内存、初始化
    
              pPepole->m_iAge = 22;
       /// 将pPepole的首地址放入rax寄存器中
       mov         rax,qword ptr [pPepole] 
       /// 为m_iAge赋值。People中没有虚函数,所以对象的前四个字节就是成员变量m_iAge
       mov         dword ptr [rax],16h 
    
              pPepole->m_bIsWoman = false;
       /// 同理
       mov         rax,qword ptr [pPepole]
       /// 为m_bIsWoman赋值,rax+4偏移到成员变量m_bIsWoman的内存地址
       mov         byte ptr [rax+4],0 
    
              delete pPepole;
       /// ...销毁
    
              return 0;
       xor         eax,eax  
    }
    

#   8. 总结

  • 全局变量、全局静态变量、局部静态变量、常量在程序代码运行前就已经分配了内存,所以可以通过内存地址直接寻址!
  • 局部变量在运行时分配栈内存,是通过栈寄存器(rsp\rbp + 立即数)间接寻址(段 + 偏移)。
  • 成员变量是通过通用寄存器间接寻址 [段 (对象首地址) + 偏移(立即数)]。
  • 无论是变量还是常量都有内存地址和内存空间(内存地址 + 数据长度)!所以很多情况下知道内存地址 + 数据长度可以强制修改这块内存的值。

# 四、指针、引用、数组

#   1. 指针和引用

  • 一级指针和引用在汇编语言中没有任何区别!那么 C++ 为什么还要实现一个引用的写法?
    • 原因 1:简化一级指针。其实引用就是一级指针 int& 等价于 int*,不需要使用 * 访问,也不需要判空。
    • 原因 2:方便理解。引用就是变量的别名,这是 C++ 程序员对引用最常见的理解,因为引用和原来的变量使用方法别无二致!
  • 引用和指针都占用相同大小的内存空间来存储目标地址,空间的大小由地址的长度决定。32 位程序占用 4 字节(地址长度为 4 字节),64 位程序占用 8 字节。
  • 至于 C++ 中引用必须赋初值,那是 C++ 编译器作出的限制。
  • 在以前学习 C++ 时,关于网上流传的引用传递比一级指针传递快啥啥的,我现在表示否定。经过测试引用传递和一级指针传递代码一模一样!
  • 代码示例:
    int main( )
    {
       ... 
              int iVar = 10;
       mov         dword ptr [rbp+4],0Ah  
    
              int* pVar = &iVar;
       /// 定义并赋值pVar
       lea         rax,[rbp+4]  
       mov         qword ptr [rbp+28h],rax  
              (*pVar)++;
       /// 取pVar所指向的内存地址放入rax中
       mov         rax,qword ptr [rbp+28h]  
       /// 取变量iVar的值放入eax中
       mov         eax,dword ptr [rax]  
       /// 加一
       inc         eax  
       /// 取pVar所指向的内存地址
       mov         rcx,qword ptr [rbp+28h]  
       /// 将计算结果eax中的数据放入pVar所指向的内存地址
       mov         dword ptr [rcx],eax  
    
              int& rVar = iVar;
       /// 定义并赋值rVar,比较上面的pVar代码可知:引用和一级指针的汇编代码一模一样!
       lea         rax,[rbp+4]  
       mov         qword ptr [rbp+48h],rax  
              rVar++;
       mov         rax,qword ptr [rbp+48h]  
       mov         eax,dword ptr [rax]  
       inc         eax  
       mov         rcx,qword ptr [rbp+48h]  
       mov         dword ptr [rcx],eax  
    
              printf( "%d\n", iVar );
       mov         edx,dword ptr [rbp+4]  
       lea         rcx,[string "%d\n" (07FF7C747ABC8h)]  
       call        printf (07FF7C7471447h)
      
       /// 引用和一级指针的汇编代码一模一样
              printf( "%d\n", *pVar );
       mov         rax,qword ptr [rbp+28h]  
       mov         edx,dword ptr [rax]  
       lea         rcx,[string "%d\n" (07FF7C747ABC8h)]  
       call        printf (07FF7C7471447h)    
              printf( "%d\n", rVar );
       mov         rax,qword ptr [rbp+48h]  
       mov         edx,dword ptr [rax]  
       lea         rcx,[string "%d\n" (07FF7C747ABC8h)]  
       call        printf (07FF7C7471447h)   
              return 0;
       xor         eax,eax  
    }
    

#   2. 数组

  • 想同类型、连续空间
  • 数组名和指针一样,占用空间,存储数组首地址。

# 五、表达式

  • 表达式计算基本流程: 根据操作数的内存地址,取出操作数据放入寄存器 => 执行表达式指令 => 将表达式计算结果放入寄存器 => 将结果放入目标内存
  • 常用的表达式指令:
    • add:加法
    • sub:减法
    • mul(imul):乘法
    • div(idiv):除法
    • sal(shl):左移
    • sar(shr):右移
    • cmp 指令 (英文单词:compare) =》设置标志位(存储在标志寄存器中):实现各种关系运算和逻辑运算,如:==、!=、>=、<=、>、<、||、&&、!
    • ...
    • 注意:如果汇编指令和 C++ 代码不对应,可能是被汇编编译器优化了!比如:3*2 = 3<<1,因为左移指令速度快于乘法指令,所以会被优化。

# 六、流程控制语句

  • C++ 中的 if、switch、while、for、?等流程控制语句,在汇编语言中都是 cmp + jump 等指令实现

    • C++ 被抛弃的 goto 语句和汇编语言的 jump 等指令,在原理和使用方法上惊人的相似!
  • jump 等指令

    • jmp 目标地址 =》代码跳转到目标地址
      • C++ 的流程控制语句中 jump 的目标地址都是段内地址,甚至都是函数体内地址,所以一般都是 =》 jump short 目标地址
      • 当然汇编语言的 jump 等指令也有能力将代码在段与段之间跳跃!
    • jge 目标地址 =》大于等于跳转(jump if greater or equal)
    • jle 目标地址 =》小于等于跳转
    • jnz 目标地址 =》如果不等于 0 则跳转(jump if not zero)
    • je 目标地址 =》等于则跳转
    • ...
  • 汇编代码示例:

    int main( )
    {
      ... 
          int iVar = 0;
      00007FF6D7105E6B  mov         dword ptr [iVar],0  
          for ( ; iVar < 5; ++iVar )
      /// 初始化语句只执行一次,当然我这这个for没有初始化语句块
      /// 第一次执行跳过自增语句块++iVar
      00007FF6D7105E72  jmp         main+2Ch (07FF6D7105E7Ch) 
      /// ++iVar
      00007FF6D7105E74  mov         eax,dword ptr [iVar]  
      00007FF6D7105E77  inc         eax  
      00007FF6D7105E79  mov         dword ptr [iVar],eax 
      /// iVar<5 cmp比较iVar和5的大小,然后设置标志寄存器中的相关标志位
      00007FF6D7105E7C  cmp         dword ptr [iVar],5  
      /// 判断前面cmp影响的标志寄存器结果,如果满足大于等于则跳转到目标地址,也就是for之外的第一句代码
      00007FF6D7105E80  jge         main+0F0h (07FF6D7105F40h)  
          {
      	    printf("%d\n", iVar);
      00007FF6D7105E86  mov         edx,dword ptr [iVar]  
      00007FF6D7105E89  lea         rcx,[string "%d\n" (07FF6D710ABC8h)]  
      00007FF6D7105E90  call        _vfprintf_l (07FF6D7101447h)  
      	    if ( iVar > 3 )
      /// 比较大小,设置标志寄存器中的标志位
      00007FF6D7105E95  cmp         dword ptr [iVar],3  
      /// 根据相关标志寄存器的结果,结果为小于等于则跳转
      00007FF6D7105E99  jle         main+5Ch (07FF6D7105EACh)  
      	    {
      		    printf( "%d\n", iVar );
      00007FF6D7105E9B  mov         edx,dword ptr [iVar]  
      00007FF6D7105E9E  lea         rcx,[string "%d\n" (07FF6D710ABC8h)]  
      00007FF6D7105EA5  call        _vfprintf_l (07FF6D7101447h)  
      	    }
      /// 前面的if已经被执行,所以要跳过else的代码,跳转到下一段switch的代码
      00007FF6D7105EAA  jmp         main+6Bh (07FF6D7105EBBh)  
      	    else
      	    {
      		    printf( "%d\n", iVar );
      00007FF6D7105EAC  mov         edx,dword ptr [iVar]  
      00007FF6D7105EAF  lea         rcx,[string "%d\n" (07FF6D710ABC8h)]  
      00007FF6D7105EB6  call        _vfprintf_l (07FF6D7101447h)  
      	    }
    
      	    switch (iVar)
      /// 判断iVar的值和关键数据匹配,匹配成功则跳转到目标Case语句块
      00007FF6D7105EBB  mov         eax,dword ptr [iVar]  
      00007FF6D7105EBE  mov         dword ptr [rbp+0D4h],eax  
      00007FF6D7105EC4  cmp         dword ptr [rbp+0D4h],0  
      00007FF6D7105ECB  je          main+9Ah (07FF6D7105EEAh)  
      00007FF6D7105ECD  cmp         dword ptr [rbp+0D4h],1  
      00007FF6D7105ED4  je          main+0ABh (07FF6D7105EFBh)  
      00007FF6D7105ED6  cmp         dword ptr [rbp+0D4h],2  
      00007FF6D7105EDD  je          main+0BCh (07FF6D7105F0Ch)  
      00007FF6D7105EDF  cmp         dword ptr [rbp+0D4h],3  
      00007FF6D7105EE6  je          main+0CDh (07FF6D7105F1Dh)  
      /// 都没匹配上则跳转到default语句块
      00007FF6D7105EE8  jmp         main+0DCh (07FF6D7105F2Ch)  
      	    {
      	    case 0:
      		    printf( "%d\n", iVar );
      00007FF6D7105EEA  mov         edx,dword ptr [iVar]  
      00007FF6D7105EED  lea         rcx,[string "%d\n" (07FF6D710ABC8h)]  
      00007FF6D7105EF4  call        _vfprintf_l (07FF6D7101447h)  
      		    break;
      /// 跳出Switch语句
      00007FF6D7105EF9  jmp         main+0EBh (07FF6D7105F3Bh)  
      	    case 1:
      		    printf( "%d\n", iVar );
      00007FF6D7105EFB  mov         edx,dword ptr [iVar]  
      00007FF6D7105EFE  lea         rcx,[string "%d\n" (07FF6D710ABC8h)]  
      00007FF6D7105F05  call        _vfprintf_l (07FF6D7101447h)  
      		    break;
      00007FF6D7105F0A  jmp         main+0EBh (07FF6D7105F3Bh)  
      	    case 2:
      		    printf( "%d\n", iVar );
      00007FF6D7105F0C  mov         edx,dword ptr [iVar]  
      00007FF6D7105F0F  lea         rcx,[string "%d\n" (07FF6D710ABC8h)]  
      00007FF6D7105F16  call        _vfprintf_l (07FF6D7101447h)  
      		    break;
      00007FF6D7105F1B  jmp         main+0EBh (07FF6D7105F3Bh)  
      	    case 3:
      		    printf( "%d\n", iVar );
      00007FF6D7105F1D  mov         edx,dword ptr [iVar]  
      00007FF6D7105F20  lea         rcx,[string "%d\n" (07FF6D710ABC8h)]  
      00007FF6D7105F27  call        _vfprintf_l (07FF6D7101447h)  
      /// 这里我故意没有写break,当执行case 3时,因为没有C++代码没有break,所以这里不会跳出switch
      /// 继续执行后面的代码
      	    default:
      		    printf( "%d\n", iVar );
      00007FF6D7105F2C  mov         edx,dword ptr [iVar]  
      00007FF6D7105F2F  lea         rcx,[string "%d\n" (07FF6D710ABC8h)]  
      00007FF6D7105F36  call        _vfprintf_l (07FF6D7101447h)  
      		    break;
      	    }
          }
      /// for循环末尾,跳转回去,继续for循环
      /// 注意跳转的目标地址,其中for的初始化语句不会再执行,直接执行++iVar语句和条件判断语句
      00007FF6D7105F3B  jmp         main+24h (07FF6D7105E74h)  
          
      /// 同理
          while ( --iVar )
      00007FF6D7105F40  mov         eax,dword ptr [iVar]  
      00007FF6D7105F43  dec         eax  
      00007FF6D7105F45  mov         dword ptr [iVar],eax  
      00007FF6D7105F48  cmp         dword ptr [iVar],0  
      00007FF6D7105F4C  je          main+10Fh (07FF6D7105F5Fh)  
          {
      	    printf( "%d\n", iVar );
      00007FF6D7105F4E  mov         edx,dword ptr [iVar]  
      00007FF6D7105F51  lea         rcx,[string "%d\n" (07FF6D710ABC8h)]  
      00007FF6D7105F58  call        _vfprintf_l (07FF6D7101447h)  
          }
      00007FF6D7105F5D  jmp         main+0F0h (07FF6D7105F40h)  
    
          return 0;
      00007FF6D7105F5F  xor         eax,eax  
    }
    
  • 我的微信
  • 这是我的微信扫一扫
  • weinxin
  • 我的微信公众号
  • 我的微信公众号扫一扫
  • weinxin