Jun 10, 2024
安全

逆向基础 - 汇编 C 语言类型

不同于寻常的C语言基本数据类型的学习,这里以汇编的形式来学习不同数据类型的存储方式和差异。

逆向基础 - 汇编 C 语言类型
Posted by
SYSTEM· 90 次阅读
专栏系列逆向基础

C语言的数据类型

C语言的基本类型属于C语言的数据类型的一部分:

image-20210302133616713
image-20210302133616713

这里先从最简单的基本类型进行入手学习,在学习基本类型之前再温故一下先前学习过的汇编的数据类型

汇编的数据类型

数据类型名称位
BYTE字节8BIT
WORD字=2字节16BIT
DWORD双字=4字节32BIT

整数类型

C语言的整数类型有:char short int long

整数类型位字节对应汇编
char8BIT1字节byte
short16BIT2字节word
int32BIT4字节dword
long32BIT4字节dword

大家可能会觉得疑惑,为什么int和long所表示的貌似一样?

这其实是历史遗留问题,在以前的16位计算机中,int的长度位2字节,但是在32位计算机中,int类型变成了4字节,而long类型原来便是4字节,现在在仍然是4字节


存储方式

接下来从汇编的角度来看看整数类型如何存储

C
#include "stdafx.h"
int main(int argc, char* argv[]) {
    char a=0xFF;
    short b=0xFF;
    int c=0xFF;
    long d=0xFF;
    return 0;
}
image-20210302141102561
image-20210302141102561
image-20210302141230344
image-20210302141230344
语句对应汇编单位
char a=0xFF;mov         byte ptr [ebp-4],0FFhbyte
short b=0xFF;mov         word ptr [ebp-8],offset main+20h (0040d4e0)word
int c=0xFF;mov         dword ptr [ebp-0Ch],0FFhdword
long d=0xFF;mov         dword ptr [ebp-10h],0FFhdword

超出数据宽度赋值

上面的赋值都是在基本类型的数据宽度之内,那么如果超出数据宽度会如何?

修改赋值的内容为0x123456789

C
#include "stdafx.h"
int main(int argc, char* argv[]) {
        char a=0x123456789;
        short b=0x123456789;
        int c=0x123456789;
        long d=0x123456789;
        return 0;
}

然后再观察反汇编代码

image-20210302142033407
image-20210302142033407
image-20210302142054391
image-20210302142054391
语句对应汇编实际赋值单位
char a=0x123456789;mov         byte ptr [ebp-4],89h89hbyte
short b=0x123456789;mov         word ptr [ebp-8],offset main+20h (0040d4e0)6789hword
int c=0x123456789;mov         dword ptr [ebp-0Ch],23456789h23456789hdword
long d=0x123456789;mov         dword ptr [ebp-10h],23456789h23456789hdword

不难发现,所有赋值语句全部都高位截断了,即高位的部分全部舍去,只赋值了数据的低位

有符号数和无符号数分析

整数类型分为有符号(signed)和无符号(unsigned)两种

默认就是有符号的类型

通过汇编观察有符号和无符号在内存中存储时是否有差别,这里以char 为例

C
#include "stdafx.h"
int main(int argc, char* argv[])
{
        char signed a=0xFF;
        char unsigned b=0xFF;
        return 0;
}
image-20210302143418909
image-20210302143418909

我们发现无符号数和有符号数在内存存储中并无差别,再一次印证了我们前面所学的:计算机并不关心数据是有符号数还是无符号数,决定一个数据是有符号数还是无符号数的是使用数据的我们,同一个数据使用不同的方式来解析

了解了有符号数和无符号数的本质后,再来谈谈有符号数和无符号数的注意事项

有符号数和无符号数注意场景

  • 类型转换

  • 比较大小

  • 数学运算

比较大小

同为有符号数时
C
#include "stdafx.h"
int main(int argc, char* argv[])
{
        char a=0xFF;
        char b=1;
        if(a>b){
                printf("a>b\n");
        }else if(a<b){
                printf("a<b\n");
        }else{
                printf("a=b\n");
        }
        return 0;
}
image-20210302144427893
image-20210302144427893
同为无符号数时
C
#include "stdafx.h"
int main(int argc, char* argv[])
{
        char unsigned a=0xFF;
        char unsigned b=1;
        if(a>b){
                printf("a>b\n");
        }else if(a<b){
                printf("a<b\n");
        }else{
                printf("a=b\n");
        }
        return 0;
}
image-20210302144656770
image-20210302144656770

汇编比较大小

看到这里,可能就会产生疑问,既然在内存中数据的存储是一样的,那么如何进行比较呢?

这里就要梦回先前的汇编跳转和比较指令

char的比较

我们可以看到:

比较有符号数
C
char a=0xFF;
char b=1;
if(a>b){
        printf("a>b\n");
}
image-20210302151435127
image-20210302151435127
比较无符号数
C
char unsigned a=0xFF;
char unsigned b=1;
if(a>b){
        printf("a>b\n");
}
image-20210302151308701
image-20210302151308701

我们发现这里的使用的jcc语句都是jle:jump less equal 小于等于才跳转(有符号数),和我们的a>b正好相反

但是我们会发现在有符号数那里使用了movsx指令,该指令为汇编语言数据传送指令MOV的变体。带符号扩展,并传送。

即它会将char从原本的byte扩展到dword,这样一来数据长度被扩展以后,自然就可以使用同一个jle指令来进行比较

int的比较

前面char的比较是通过数据宽度的扩展来实现比较的,那么当使用int时,无法扩展符号的数据宽度时,如何比较?

将上面的char改为int后再次观察反汇编代码

比较有符号数
C
int a=0xFF;
int b=1;
if (a > b) {
    printf("a>b\n");
}
image-20210302152925765
image-20210302152925765
比较无符号数
C
int unsigned a=0xFF;
int unsigned b=1;
if (a > b) {
    printf("a>b\n");
}
image-20210302153115591
image-20210302153115591
差别

我们可以发现,同样是比较,比较无符号数和有符号数时,分别对应两个不同的jcc语句

比较jcc语句含义
比较有符号数jle main+3Dh(0040d7ed)小于等于则跳转 (有符号数)
比较无符号数jbe main+3Dh(0040d7ed)小于等于则跳转 (无符号数)

浮点类型

C语言的浮点类型分为float和double

存储方式和规范

float和double在存储方式上都是遵从IEEE规范的

float的存储方式如下图所示:

image-20210302153815823
image-20210302153815823

double的存储方式如下图所示:

image-20210302153846569
image-20210302153846569

由于double的长度比较长,我们下面就用float作为例子,实际上,double不过是比float精度更高了,在将浮点数转化为存储到内存中的二进制的步骤几乎一致

转化步骤

将一个float型转化为内存存储格式的步骤为:

  1. 先将这个实数的绝对值化为二进制格式

  2. 将这个二进制格式实数的小数点左移或右移n位,直到小数点移动到第一个有效数字的右边

  3. 从小数点右边第一位开始数出二十三位数字放入第22到第0位。

  4. 如果实数是正的,则在第31位放入“0”,否则放入“1”

  5. 如果n是左移得到的,说明指数是正的,第30位放入“1”。如果n是右移得到的或n=0,则第30位放入“0”

  6. 如果n是左移得到的,则将n减去1后化为二进制,并在左边加“0”补足七位,放入第29到第23位。 如果n是右移得到的或n=0,则将n化为二进制后在左边加“0”补足七位,再各位求反,再放入第29到第23位

看起来很复杂QAQ,但结合下面的实例来看就还好(。・∀・)ノ゙

在转化步骤中的第一步,又分为整数部分的二进制化和小数部分的二进制化

十进制整数二进制化

采用除留余数法

比如将11转化成二进制数

计算余数
11/2=51
5/2=21
2/2=10
1/2=01
0结束

11二进制表示为(从下往上):1011

注意到只要除以后结果为0便结束了,任意整数不断除以2最终都会等于0,因此所有整数都可以用二进制来精确表示

十进制小数二进制化

采用乘二取整法

比如将0.9转化为二进制数

计算取整数部分
0.9*2=1.81
0.8(前面结果的小数部分)*2=1.61
0.6*2=1.21
0.2*2=0.40
0.4*2=0.80
0.8*2=1.61
0.6*2=1.21
…………

0.9二进制表示为(从上往下)……

很显然,上面的计算过程循环了,也就是说*2永远不可能消灭小数部分,这样算法将无限下去。很显然,并非所有的小数都可以用二进制精确表示,就和十进制里也无法精确表示出1/3一样

实例

8.25f

C
#include "stdafx.h"
int main(int argc, char* argv[])
{
        float i=8.25f;
        return 0;
}

我们用反汇编查看8.25f

image-20210302165607153
image-20210302165607153
X86ASM
00401028   mov         dword ptr [ebp-4],41040000h

我们会发现8.25f的表现形式为41040000h,接下来我们就来研究这个41040000h是怎么来的

一步步按先前提到的转化步骤来:

实数的绝对值化为二进制格式

先处理整数部分8.25

计算余数
8/2=40
4/2=20
2/2=10
1/2=01

整数部分8转化为二进制:1000(从下往上)

再处理小数部分8.25

计算取整数部分
0.25*2=0.50
0.5*2=1.01

0.25转化为二进制可表示为:01(从上往下)

于是8.25用二进制可表示为1000.01

填充尾数

接下来先填充尾数部分

先将二进制数转为用科学计数法表示

1000.01=1.00001*2的3次方        (小数点向左移动3位 指数为3)

1.**00001***2

就是将小数点后面的23位填入尾数部分,我们这里小数部分恰好能够精确地转化为二进制数,于是剩下的部分用0填充即可

如果是前面的0.9转化为的0.110011……=1.1100……*2的-1次方

则是截取小数点后面的23位填入1100……

符号位指数部分尾数部分
占用空间1823
存储内容00001000000000000000000

此时尾部部分已经填充完毕,再填充符号位

填充符号位

符号位就简单多了,正数填充0,负数填充1即可

这里我们的8.5f是正数,填0结束

符号位指数部分尾数部分
占用空间1823
存储内容000001000000000000000000
填充指数部分

指数部分的最高位填充看前面是前面将二进制数科学计数法化时,是进行了左移还是右移,左移填1,右移填0

前面我们得到了8.5f的转化:

1000.01=1.00001*2的3次方        (小数点向左移动3位 指数为3)

很明显我们的是左移,因此,最高位填写1

剩下的部分则是用指数减去1后二进制化填充即可

8.5f的指数为3,3-1=2

2的二进制为10

所以指数部分应该为:

10000010

于是整个填充完成

符号位指数部分尾数部分
占用空间1823
存储内容01000001000001000000000000000000

总共为0100 0001 0000 0100 0000 0000 0000 0000

转化为十六进制为4 1 0 4 0 0 0 0 0

和我们前面用汇编看到的结果一致

-8.25f

C
#include "stdafx.h"
int main(int argc, char* argv[])
{
        float i=-8.25f;
        return 0;
}
image-20210302170100469
image-20210302170100469
X86ASM
00401028   mov         dword ptr [ebp-4],0C1040000h

-8.25f和8.25f相差的只有符号位,于是将前面的8.25f的符号位改为1即可

符号位指数部分尾数部分
占用空间1823
存储内容11000001000001000000000000000000

也就是1100 0001 0000 0100 0000 0000 0000 0000

转为十六进制是 C 1 0 4 0 0 0 0

和前面反汇编看到的结果一致

0.25f

这次看个纯小数在内存中如何存储,依旧采取相同的套路

实数的绝对值化为二进制格式
计算取整数部分
0.25*2=0.50
0.5*1=1.01

0.25f转化为二进制:0.01

填充尾数

科学计数法表示:0.01=1.0*2的-2次方        (小数点向右移动2位 指数为-2 )

尾数填充0,1.0小数点后面全为0,于是尾数全部填0即可

符号位指数部分尾数部分
占用空间1823
存储内容00000000000000000000000
填充符号位

0.25f是正数直接填充0即可

符号位指数部分尾数部分
占用空间1823
存储内容000000000000000000000000
填充指数部分

科学计数法表示:0.01=1.0*2的-2次方        (小数点向右移动2位 指数为-2 )

向右移动,指数部分最高位填0

指数为-2,-2-1=-3

将-3转化为二进制:-3的十六进制对应fd,fd转为二进制111 1101

不知道为什么-3对应fd的可以看这里:逆向基础笔记二 数据宽度和逻辑运算

我们发现-3转为二进制共有8位,但是指数部分总共也只有8位,其中最高位还用作表示是右移填充了0,于是只截取低7位填充进指数部分剩下的内容

于是整个填充完成

符号位指数部分尾数部分
占用空间1823
存储内容00111110100000000000000000000000

总共为0011 1110 1000 0000 0000 0000 0000 0000

转化为十六进制为3 e 8 0 0 0 0 0

接下来用反汇编验证一下:

image-20210302172343260
image-20210302172343260

得到的结果是一致的,好耶ヽ(✿゚▽゚)ノ

评论交流
暂无评论,欢迎分享您的见解