C++ 反汇编
配套资源下载
环境及工具
反汇编引擎工作原理(可略过)
基本数据类型的表现形式
程序的真正入口
本文档使用 MrDoc 发布
-
+
首页
基本数据类型的表现形式
## 整数 - **int 类型与 long 类型都占 4 字节,short 类型占 2 字节,long long 类型占 8 字节。** - 由于二进制数不方便显示和阅读,因此内存中的数据采用十六进制数表示。 1 字节由 2 个十六进制数组成,在进制转换中,1 个十六进制数可用 4 个二进制数表示,每个二进制数表示 1 位,因此 **1 字节在内存中占 8 位**。 ### 无符号整数 - 取值范围为 0x00000000~0xFFFFFFFF,如果转换为十进制数,则表示范围为 **0~4294967295**。 - 当无符号整型不足 32 位时,用 0 来填充剩余高位,直到**占满 4 字节内存空间为止**。 - 在内存中以**“小尾方式”**存放。“小尾方式”存放以字节为单位,按照数据类型的长度,**低数据位放在内存的低端,高数据位放在内存的高端**,如 0x12345678,会存储为 78 56 34 12(这里的 78,是最低的个位和十位)。 - 无符号整数不存在正负之分,都是正数。 ### 有符号整数 - 有符号整数中用来表示符号的是最高位,即符号位。**最高位为 0 表示正数,最高位为 1 表示负数。**因此有符号整数的取值范围要比无符号整数取值范围少 1 位,即 0x80000000~0x7FFFFFFF,如果转换为十进制数,则表示范围为 -2 147 483 648~2 147 483 647。 - **负数在内存中都是以补码形式存放的,补码的规则是用 0 减去这个数的绝对值,也可以简单地表达为对这个数值取反加 1。** - 为什么负数要用补码来存,因为为了实现 i+(-i)=0,正负数相加等于零的效果。 - 查看用十六进制数表示时的最高位,最高位小于 8 则为正数,大于等于 8 则为负数。 - 在内存中判断是有符号还是无符号的办法:根据指令或者已知 API 的定义进行判断。比如 MessageBoxA,第四个参数是无符号整数。 ## 浮点数 ### 定点实数的优缺点 - 可以约定两个高字节存放整数部分,两个低字节存储小数部分。好处是计算效率高,缺点也显而易见:存储不灵活,比如我们想存储 65536.5,由于整数的表达范围超过了 2 字节,就无法用定点实数存储方式了。 ### 浮点数的存储方式 - 浮点实数存储方式,道理很简单,就是**用一部分二进制位存放小数点的位置信息**,我们可以称之为“指数域”,**其他的数据位用来存储没有小数点时的数据和符号**,我们可以称之为“数据域”“符号域”。在访问时取得指数域,与数据域运算后得到真值,如 67.625,利用浮点实数存储方式,数据域可以记录为 67625,小数点的位置可以记为 10 的-3 次方,对该数进行访问时计算一下即可。 - float(单精度)、double(双精度)。**float 在内存中占 4 字节,double 在内存中占 8 字节。** - 浮点类型并不是将一个浮点小数直接转换成二进制数保存,而是将浮点小数转换成的二进制码重新编码,再进行存储。**C/C++的浮点数是有符号的。** - 在 C/C++中,将浮点数强制转换为整数时,**不会采用数学上四舍五入的方式,而是舍弃掉小数部分**(也就是“向零取整”),不会进位。 - 浮点数的操作不会用到通用寄存器,而是会**使用浮点协处理器的浮点寄存器,专门对浮点数进行运算处理。** #### float 类型的 IEEE 编码 - float类型在内存中占 4 字节(32 位)。最高位用于表示符号,在剩余的 31 位中,从左向右取 8 位表示指数,其余表示尾数。 ![](/media/202404/2024-04-02_095831_8360930.5635710709382474.png) - **举例一定要看** - 举例:float 类型的 12.25f 转换为 IEEE 编码,须将 12.25f 转换成对应的二进制数 1100.01,整数部分为 1100,小数部分为 01(小数部分的 1 表示 2^-1 也就是 0.5,01 表示 2^-2 也就是 0.25);小数点向左移动,每移动 1 次,指数加 1,移动到**除符号位的最高位为 1 处**,停止移动,这里**移动 3 次**。对 12.25f 进行科学记数法转换后二进制部分为1.10001,指数部分为 3。**在 IEEE 编码中,由于在二进制情况下,最高位始终为 1(二进制科学计数法肯定最高位是 1),为一个恒定值,故将其**`忽略不计`。这里是一个正数,所以**符号位添加 0**。 - 转换过程如下: - 符号位:0。 - 指数位:十进制 3+127=130,转换为二进制为 10000010。 - 尾数位:10001 000000000000000000(**当不足 23 位时,低位补 0 填充,最高位的 1 默认去掉)**。 - 为什么**指数位要加 127**呢?这是因为**指数可能出现负数**,十进制数 127 可表示为二进制数 01111111,IEEE 编码方式规定,当指数小于 0111111 时为一个负数,反之为正数,因此 01111111 为 0。 - 此时 12.25f 转换成二进制是 **0 10000010 10001000000000000000000(对比上面结果认真思考下这个数字的由来)**。转换成十六进制是 0x41440000,内存中以小尾方式进行排列,故为 00 00 44 41。 - 举例 2: - -0.125f 经转换后二进制部分为 0.001,用科学记数法表示为 1.0,指数为-3。 - -0.125f IEEE转换后各位的情况如下。 - 符号位:1。 - 指数位:十进制127+(-3),转换为二进制是 01111100,如果不足 8 位,则高位补 0。 - 尾数位:00000000000000000000000。 - -0.125f 转换后的 IEEE 编码二进制拼接为 10111110000000000000000000000000。转换成十六进制数为 0xBE000000,内存中显示为 00 00 00 BE。 - 举例 3: - 1.3f 转换后的 IEEE 编码二进制拼接为 00111111101001100110011001100110。转换成十六进制数为 0x3FA66666,在内存中显示为 66 66 A6 3F。由于在转换二进制过程中产生了无穷值,舍弃了部分位数,所以进行 IEEE 编码转换后得到的是一个近似值,存在一定的误差。再次将这个 IEEE 编码值转换成十进制小数,得到的值为 1.2516582,四舍五入保留一位小数之后为 1.3。**这就解释了为什么 C++ 在比较浮点数值是否为 0 时(C/C++比较浮点数不能使用==),要做一个区间比较而不是直接进行等值比较。** - 浮点数的正确比较方式 ```c float f1 = 0.0001f; // 精确范围 if (f2 >= -f1 && f2 <= f1) { // f1等于0 } ``` #### double 的存储方式 - double 和 float 大同小异,double 类型占 8 字节的内存空间,同样,最高位也用于表示符号,**指数位占 11 位,剩余 52 位表示位数。** ### 基本浮点数指令(看看就好,遇到了搜下就行) - 浮点数操作是通过浮点寄存器实现的,而普通数据类型使用的是通用寄存器,它们分别**使用两套不同的指令。** - 在早期 CPU 中,浮点寄存器是通过栈结构实现的,由 ST(0)~ ST(7)共 8 个栈空间组成,每个浮点寄存器占8字节。**每次使用浮点寄存器都是率先使用ST(0),而不能越过 ST(0)直接使用 ST(1)**。浮点寄存器的使用就是压栈、出栈的过程。当 ST(0)中存在数据时,**执行压栈操作后,ST(0)中的数据将装入 ST(1)中,如无出栈操作,将按顺序向下压栈,直到将浮点寄存器占满为止**。常用浮点数指令的介绍如表所示,其中,IN 表示操作数入栈,OUT 表示操作数出栈。 | 指令名称 | 使用格式 | 指令功能 | | :---: | :---: | :---: | | FLD | FLD IN | 将浮点数 IN 压入 ST(0)中。IN(mem 32/64/80) | | FILD | FILD IN | 将整数 IN 压入 ST(0)中。IN(mem 32/64/80) | | FLDZ | FLDZ | 将 0.0 压入 ST(0)中 | | FLD1 | FLD1(不知道是 1 还是 l) | 将 1.0 压入 ST(0)中 | | FST | FST OUT | ST(0) 中的数据以浮点形式存入 OUT 地址中。OUT(mem 32/64) | | FSTP | FSTP OUT | 和 FST 指令一样,但会执行一次出栈操作 | | FIST | FIST OUT | ST(0) 中的数据以整数形式存入 OUT 地址中。OUT(mem 32/64) | | FISTP | FISTP OUT | 和 FIST 指令一样,但会执行一次出栈操作 | | FCOM | FCOM IN | 将 IN 地址数据与 ST(0)进行史书比较,影响对应标记位 | | FTST | FTST | 比较 ST(0)是否为 0.0,影响对应标记位 | | FADD | FADD IN | 将 IN 地址内的数据与 ST(0)做加法运算,结果放入 ST(0)中 | | FADDP | FADDP ST(N),ST | 将 ST(N)中的数据与 ST(0)中的数据做加法运算,N 为 0~7 中的任意一个数,先执行一次出栈操作,然后将相加结果放入 ST(0)中保存 | - **其他运算指令和普通指令类似,只须在前面加 F 即可,如 FSUB 和 FSUBP 等。** - 在使用浮点指令时,都要先利用 ST(0)进行运算。当 ST(0)中有值时,便会将 ST(0)中的数据顺序向下存放到 ST(1)中,然后再将数据放入 ST(0)。如果再次操作 ST(0),则会先将 ST(1)中的数据放入 ST(2),然后将 ST(0)中的数据放入 ST(1),最后将新的数据存放到 ST(0)。以此类推,在 8 个浮点寄存器都有值的情况下继续向 ST(0)中的存放数据,这时会丢弃 ST(7)中的数据信息。 - 1997 年开始,Intel 和 AMD 都引入了**媒体指令(MMX)**,这些指令允许多个操作并行,允许对多个不同的数据并行执行同一操作。近年来,这些扩展有了长足的发展。名字经过了一系列的修改,从 MMX 到 **SSE(流 SIMD 扩展)**,以及最新的 **AVX(高级向量扩展)**。每一代都有一些不同的版本。每个扩展都用来管理寄存器中的数据,这些寄存器在 MMX 中被称为 **MM 寄存器**,在 SSE 中被称为 **XMM 寄存器**,在 AVX 中被称为 **YMM 寄存器**。**MM 寄存器是 64 位的,XMM 是 128 位的,而 YMM 是 256 位的。**每个 YMM 寄存器可以存放 8 个 32 位值或 4 个 64 位值,可以是整数,也可以是浮点数。**YMM 寄存器一共有 16 个(YMM0~YMM15),而 XMM 是 YMM 的低 128 位**。常用 SSE 浮点数指令的介绍如表所示。**(简单来说就是寄存器的扩展)** | 指令名称 | 使用格式 | 指令功能 | | :-----: | :-----: | :-----: | | MOVSS | xmm1,xmm2<br>xmm2,mem32<br>xmm2/mem32,xmm1 | 传送单精度数 | | MOVSD | xmm1,xmm2<br>xmm2,mem64<br>xmm2/mem64,xmm1 | 传送双精度数 | | MOVAPS | xmm1,xmm2/mem128<br>xmm1/mem128,xmm2 | 传送对齐的封装好的单精度数 | | MOVAPD | xmm1,xmm2/mem128<br>xmm1/mem128,xmm2 | 传送对齐的封装好的双精度数 | | ADDSS | xmm1,xmm2/mem32 | 单精度加法 | | ADDSD | xmm1,xmm2/mem64 | 双精度加法 | | ADDPS | xmm1,xmm2/mem128 | 并行 4 个单精度加法 | | ADDPD | xmm1,xmm2/mem128 | 并行 2 个双精度加法 | | SUBSS | xmm1,xmm2/mem32 | 单精度数减法 | | SUBSD | xmm1,xmm2/mem64 | 双精度数减法 | | SUBPS | xmm1,xmm2/mem128 | 并行 4 个单精度数减法 | | SUBPD | xmm1,xmm2/mem128 | 并行 2 个双精度减法 | | MULSS | xmm1,xmm2/mem32 | 单精度数乘法 | | MULSD | xmm1,xmm2/mem64 | 双精度数乘法 | | MULPS | xmm1,xmm2/mem128 | 并行 4 个单精度数乘法 | | MULPD | xmm1,xmm2/mem128 | 并行 2 个双精度数乘法 | | DIVSS | xmm1,xmm2/mem32 | 单精度数除法 | | DIBSD | xmm1,xmm2/mem64 | 双精度数除法 | | DIVPS | xmm1,xmm2/mem128 | 并行 4 个单精度数除法 | | DIVPD | xmm1,xmm2/mem128 | 并行 2 个双精度数除法 | | CVTTSS2SI | reg32,xmm1/mem32<br>reg64,xmm1/mem64 | 用截断的方法将单精度数转换为整数 | | CVTTSD2SI | reg32,xmm1/mem32<br>reg64,xmm1/mem64 | 用截断的方法将双精度数转换为整数 | | CVTSI2SS | xmm1,reg32/mem32<br>xmm1,reg64/mem64 | 将整数转换为单精度数 | | CVTSI2SD | xmm1,reg32/mem32<br>xmm1,reg64/mem64 | 将整数转换为双精度数 | - 从示例中可以发现,float 类型的浮点数虽然占 4 字节,但是使用浮点栈将以 8 字节方式进行处理,而使用媒体寄存器则以 4 字节处理。浮点数作为返回值的情况也是如此,在 32 位程序中使用浮点栈 st(0)作为返回值同样需要传递 8 字节数据,64 位程序中使用媒体寄存器 xmm0 作为返回值只需要传递 4 字节 - 书上虽然已经把反汇编代码贴出来了,但是还是建议实际动手操作一遍。 ## 字符和字符串 - **在 C++中,以'\0'作为字符串结束标记**。每个字符都记录在一张表中,它们各自对应一个唯一编号,系统通过这些编号查找到对应的字符并显示。 ### 字符的编码 - 在C++中,字符的编码格式分为两种:**ASCII 和 Unicode**。 - ASCII 编码在内存中**占 1 字节**,由 0~255 之间的数字组成。 - Unicode **占双字节**、表示范围为 0~65535。 - **char** 定义 ASCII 编码格式的字符,使用 **wchar_t** 定义 Unicode 编码格式的字符。**wchar_t 中保存 ASCII 编码,不足位补 0**,如字符'a'的 ASCII 编码为 0x61,Unicode 编码为 0x0061。 - **ASCII 编码与 Unicode 编码都可以用来存储汉字**,但是它们对汉字的编码方式各不相同。 - ASCII 使用 GB2312-80,又名汉字国标码,保存了 6763 个常用汉字编码,用两个字节表示一个汉字。。在 GB2312-80 中用区和位来定位,第一个字节保存每个区,共 94 个区;第二个字节保存每个区中的位,共 94 位。详细信息可查看 GB2312-80 编码的说明。 - **Unicode 使用 UCS-2 编码格式,最多可存储 65536 个字符**。汉字博大精深,其中有简体字、繁体字,还有网络中流行的火星文,它们的总和远远超过了 UCS-2 的存储范围,所以 UCS-2 编码格式中只保存了常用字。为了将所有的汉字都容纳进来,Unicode 也采用了与 ASCII 类似的方式——**用两个 Unicode 编码(4字节)解释一个汉字,一般称之为 UCS-4 编码格式**。UCS-2 编码表的使用和 ASCII 编码表的使用是一样的,每个数字编号在表中对应一个汉字,从 0x4E00 到 0x9520 为汉字编码区。例如,在 UCS-2 中,**“烫”字的编码为 0x70EB**。 ### 字符串的存储方式 - 字符串的长度保存 1. **在首地址的 4 字节中保存字符串的总长度**。 - 优点:**获取字符串长度时,不用遍历字符串中的每个字符**,取得首地址的前 n 字节就可以得到字符串的长度(n 的取值一般是 1、2、4)。 - 缺点:字符串长度**不能超过** n 字节的表示范围,且要多**开销** n 字节空间保存长度。如果涉及通信,双方交互前必须事先知道通信字符串的长度。 2. **在字符串的结尾处使用一个规定好的特殊字符,即结束符**。 - 优点:**没有记录长度的开销**,即不需要存储空间记录长度信息;另外,如果涉及通信,通信字符串可以根据实际情况随时结束,结束时附上结束符即可。 - 缺点:**获取字符串长度需要遍历所有字符**,寻找特殊结尾字符,在某些情况下处理效率低。 - C++使用结束符'\0'作为字符串结束标志。**ASCII 编码使用一个字节'\0',Unicode 编码使用两个字节'\0'**。 - 在程序中,一般都会使用一个变量来**存放字符串中第一个字符的地址**,以便于查找使用字符串。 - IDA 中,**使用快捷键 A,便可将分析地址到'\0'解释为字符串。** ## 布尔类型 - **C++中定义 0 为假,非 0 为真。使用 bool 定义布尔类型变量。布尔类型在内存中占 1 字节。** ## 地址、指针和引用 - 在 C++ 中,地址标号使用十六进制表示,取一个变量的地址使用“&”符号,**只有变量才存在内存地址,常量没有地址(不包括 const 定义的伪常量,const 常量是编译器检查是否被修改,内存中还是变量)。** - 指针的定义使用“TYPE*”格式,TYPE 为数据类型,任何数据类型都可以定义指针。指针本身也是一种数据类型,用于保存各种数据类型在内存中的地址。指针变量同样可以取出地址,所以会出现多级指针。 - 引用的定义格式为“TYPE&”,TYPE 为数据类型。在 C++中是不可以单独定义的,并且在定义时就要进行初始化。引用表示一个变量的别名,对它的任何操作本质上都是在操作它所表示的变量。 - **在 32 位应用程序中,地址是一个由 32 位二进制数字组成的值;在 64 位应用程序中,地址是一个由 64 位二进制数字组成的值。**由于指针保存的数据都是地址,所以**无论什么类型的指针,32 位程序都占据 4 字节的内存空间,64 位程序都占据 8 字节的内存空间**。 - 举例:如果是在一个int类型的指针中保存这个地址(假设为 0x0135FE04),就可以将其看作 int 类型数据的起始地址,向后数 4 字节到 0x0135FE08 处,将 0x0135FE04~0x0135FE08 中的数据按整型存储方式解释。 - 指针与地址的不同点 | 指针 | 地址 | | :----: | :----: | | 变量,保存变量地址 | 常量,内存标号 | | 可修改,再次保存其它变量地址 | 不可修改 | | 可以对其执行取地址操作 | 不可执行取地址操作 | | 包含对保存地址的解释信息 | 仅有地址值无法解释数据 | - 指针与地址的相同点 | 指针 | 地址 | | :----: | :----: | | 取出指向地址内存的数据 | 去除地址对应内存的数据 | | 对地址偏移后,取出数据 | 偏移后去数据,自身不变 | | 求两个地址值的差 | 求两个地址的差 | ### 指针的工作方式 - 指针保存的都是地址,为什么还需要类型作为修饰呢?因为我们需要用类型去解释这个地址中的数据。每种数据类型所占的内存空间不同,**指针只保存了存放数据的首地址,而没有指明该在哪里结束。这时就需要根据对应的类型来寻找解释数据的结束地址。** - **指针类型都只支持加法和减法。** - 指针加 1 后,指针内保存的地址值并不一定会加 1,运算结果取决于指针类型,如指针类型为 int,地址值将会加 4,这个 4 是根据类型大小所得到的值。当指针中保存的地址为数组首地址时,为了能够利用指针加 1 后访问到数组内下一成员,所以**加的是类型长度,而非数字 1。** ~~~ type *p; // 这里用 type 泛指某类型的指针 // 省略指针赋值代码 p+n 的目标地址 = 首地址 + sizeof( 指针类型 type) * n ~~~ - 对于偏移量为负数的情况,此公式**同样适用**。 - **两指针做减法操作是在计算两个地址之间的元素个数,结果为有符号整数。** ~~~ type *p, *q; // 这里用type泛指某类型的指针 // 省略指针赋值代码 p-q = ((int)p - (int)q) / sizeof(指针类型type) ~~~ - 两指针相加也是没有意义的。 ### 引用 - 引用类型实际上就是指针类型,只不过用于存放地址的内存空间对使用者而言是隐藏的。 - 在 C++中,除了引用是通过编译器实现寻址,而指针需要手动寻址外,**引用和指针没有太大区别**。**在反汇编下,没有引用这种数据类型。** ## 常量 - 常量是一个恒定不变的值,它**在内存中也是不可修改的**。常量数据在程序运行前就已经存在,它们**被编译到可执行文件中,当程序启动后,它们便会被加载进来**。这些数据通常都会保存在**常量数据区中,该区的属性没有写权限**,所以在对常量进行修改时,程序会报错。试图修改常量数据都将引发异常,导致程序崩溃。 ### 常量的定义 - 在 C++中,可以使用宏机制#define 来定义常量,也可以使用 const 将变量定义为一个常量。**\#define 定义常量名称,编译器在对其进行编译时,会将代码中的宏名称替换成对应信息**。宏的使用可以增加代码的可读性。**const 是为了增加程序的健壮性而存在的**。 ### #define 和 const 的区别 - **\#define 修饰的符号名称是一个真量数值,而 const 修饰的栈常量,是一个“假”常量。**const 定义的实际上还是变量,只是由编译器检查是否有修改行为。**被 const 修饰过的栈变量本质上是可以被修改的**。我们可以利用指针获取 const 修饰过的栈变量地址,强制将 const 属性修饰去掉。 ~~~c #include <stdio.h> int main(int argc, char* argv[]) { const int n1 = 5; int *p = (int*)&n1; *p = 6; int n2 = n1; return 0; } ~~~ | #define | const | | :-----: | :-----: | | 在编译期间查找替换 | 在编译期间检查 const 修饰的变量是否被修改 | | 由系统判断是否被修改 | 由编译器限制修改 | | 字符串定义在文件只读数据区,数据常量编译为立即数寻址方式,成为二进制代码的一部分 | 根据作用域决定所在的内存位置和属性 |
别卷了
2024年7月3日 00:05
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
分享
链接
类型
密码
更新密码