Jun 10, 2024
安全

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

前面提到过类型转换这个概念,但在那里只是一笔带过,重点放在了比较大小上,类型转换自然涉及到C语言的基本类型及有符号数和无符号数。

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

类型转换的使用场景

类型转换一般为由数据宽度小的转换成数据宽度大的,不然可能会有高位数据被截断的现象,引起数据丢失(不知道高位截断的可以回顾笔记十一)

需要一个变量来存储一个数据,刚开始这个数据的数据宽度较小,后来发现存不下了,需要换一个数据宽度更大的变量来存储

类型转换相关汇编指令

MOVSX

先符号扩展,再传送

X86ASM
MOV AL,0FF
MOVSX CX,AL
MOV AL,80
MOVSX CX,AL

MOVZX

先零扩展,再传送

X86ASM
MOV AL,0FF
MOVZX CX,AL
MOV AL,80
MOVSX CX,AL

类型转换例子

C
#include "stdafx.h"
int main(int argc, char* argv[])
{
        unsigned char i=0xFF;
        printf("%d\n",i);
        int j=i+1;
        i=i+1;
        printf("%d\n",i);
        printf("%d\n",j);
        return 0;
}

我们来看看以上代码的运行结果:

image-20210303124927197
image-20210303124927197

分析结果

首先输出的是一个无符号数 i,0xFF对应的十进制为255

接着输出的是i自增1后的结果,我们发现255+1变成了0,这是因为char的数据宽度为8位,最大便是0xFF了,再加上一就超出了char的数据宽度,也就是发生了上溢。于是数据变成了0

最后输出的是类型转换后i+1的结果,正确地显示为256

汇编观察

汇编代码

大致了解了产生上述结果的原因,用汇编来更透彻地分析:

image-20210303130034484
image-20210303130034484

我们这里提取出 去除printf输出的部分,得到汇编代码如下:

X86ASM
8:        unsigned char i=0xFF;
0040D708   mov         byte ptr [ebp-4],0FFh
10:       int j=i+1;
0040D722   mov         ecx,dword ptr [ebp-4]
0040D725   and         ecx,0FFh
0040D72B   add         ecx,1
0040D72E   mov         dword ptr [ebp-8],ecx
11:       i=i+1;
0040D731   mov         edx,dword ptr [ebp-4]
0040D734   and         edx,0FFh
0040D73A   add         edx,1
0040D73D   mov         byte ptr [ebp-4],dl

结果有些尴尬,,ԾㅂԾ,, 我们发现并没有用到前面所说的movx或movzx指令,但是先不着急,先看看这段汇编代码做了些什么

对应i赋值

很稀松平常的,char对应数据宽度为byte赋值

X86ASM
8:        unsigned char i=0xFF;
0040D708   mov         byte ptr [ebp-4],0FFh

对应j赋值

接下来就是j的赋值

X86ASM
10:       int j=i+1;
0040D722   mov         ecx,dword ptr [ebp-4]
0040D725   and         ecx,0FFh
0040D72B   add         ecx,1
0040D72E   mov         dword ptr [ebp-8],ecx

可以看到:首先是直接将前面的 i 赋值给ecx,并且赋值的长度为dword,很明显将超出char长度的内容也赋值到了ecx

X86ASM
mov         ecx,dword ptr [ebp-4]

然后下一句很关键

X86ASM
and ecx,0FFh

与操作,将之前多超出的部分和0相与,也就是将超出的部分全部清零(用零填充),相当于MOVZX指令的零填充

接下来就是加一的操作

X86ASM
add ecx,1

最后就是将ecx赋值给了我们的变量j

X86ASM
mov         dword ptr [ebp-8],ecx

对应i=i+1

和前面j的赋值似曾相识,直接将前面i赋值给edx,并且赋值的长度为dword,很明显将超出char长度的内容也赋值到了edx

X86ASM
mov         edx,dword ptr [ebp-4]

接着也是与操作,超出来的部分清零

X86ASM
and         edx,0FFh

接下来就是加一的操作

X86ASM
add         edx,1

最后是赋值

X86ASM
mov         byte ptr [ebp-4],dl

将edx的低8位赋值给i

小总结

通过前面的分析,可以发现,无论是 i 自己+1还是用数据宽度较高的 j 来接收 i +1的结果

期间都是要先取出超出 i 数据宽度的dword长度的数据,然后再使用and 0xFF,把超出的部分清零

换言之,char的计算也会先转换为int的计算,最后再转回来

还有就是汇编指令并非一成不变,不是一定要使用movsx或movzx指令,也可以通过这种取出超出长度的数据,然后再将超出的部分清零的操作来实现类movzx指令的结果

自写汇编实现功能

前面虽然我们分析了,汇编代码,但很可惜编译器并没有使用movzx指令来实现操作,本着学习巩固的精神,我们自己写汇编来实现上述的功能,以此来加深对movzx的理解

C
#include "stdafx.h"
unsigned char i=0xFF;
int j=0;
void _declspec (naked) func(){
        _asm{
                                //保留调用前堆栈
                push ebp
                //提升堆栈
                mov ebp,esp
                sub esp,0x40
                //保护现场
                push ebx
                push esi
                push edi
                //初始化提升的堆栈,填充缓冲区
                mov eax,0xCCCCCCCC
                mov ecx,0x10
                lea edi,dword ptr ds:[ebp-0x40]
                rep stosd
                //函数核心功能

                                //将i零扩充赋值给ecx
                                movzx ecx,i
                                //ecx自增1
                                inc ecx
                                //将ecx赋值给j
                                mov j,ecx
                                //直接让i自增1
                                inc i

                //恢复现场
                pop edi
                pop esi
                pop ebx
                //降低堆栈
                mov esp,ebp
                pop ebp                
                //返回
                ret 
        }
}

int main(int argc, char* argv[])
{
        printf("%d\n",i);
        func();
        printf("%d\n",i);
        printf("%d\n",j);
        return 0;
}

执行后的结果为:

image-20210303135845896
image-20210303135845896

和前面的执行结果一致

上面的代码看似很多,其实核心功能就只有四句:

X86ASM
//函数核心功能
//将i零扩充赋值给ecx
movzx ecx,i
//ecx自增1
inc ecx
//将ecx赋值给j
mov j,ecx
//直接让i自增1
inc i

我们这里使用了movzx指令,实现了相同的功能,也顺便引入了一个新的汇编语句:

X86ASM
inc eax 相当于 add eax,1     也就是eax=eax+1

优点是速度比add指令快,占用空间小

与之相对的便是:dec指令,自减1

X86ASM
dec eax 相当于 sub eax,1          也就是eax=eax-1

优点是速度比sub指令快,占用空间小

PS:逆向基础笔记九C语言内联汇编和调用协定

以上我们的类型转换就暂告一段落了(●ˇ∀ˇ●),虽然还有movsx 有符号的没有去分析,但其实它和movzx并无太大的差别,就是要注意一些有符号数和无符号数的应用和符号位即可。

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