# 一、简介
- 代码运行环境:VS2019、 x64 位、debug 模式。
- CPU:AMD
- 汇编语言基础
# 二、基础
# 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)!
至于数据怎么使用(相加、相减、字符合并,输出等等)都交给程序员决定,有点弱数据类型语言的味道!
- C++ 是强类型数据语言,所有的数据都必须定义类型,类型用于在 C++ 编译器中解释该类型的数据大小!从而将 C++ 数据转为汇编语言的数据。
# 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 : }
- C++ 代码
# 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 目标地址 =》等于则跳转
- ...
- jmp 目标地址 =》代码跳转到目标地址
汇编代码示例:
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 }