您当前的位置:首页 > 计算机 > 软件应用 > 反汇编工具

局部变量,全局变量与内存

时间:09-25来源:作者:点击数:

使用IDA分析局部变量

#include <stdio.h>


int main()
{
	int nNum = 1;
	float fNum = 2.5;
	char ch = 'A';

	printf("int %d, float %f, char %c", nNum, fNum, ch);

	return 0;
}

使用IDA跟进到main函数,F5调出反汇编

摁下tab键,进入汇编界面,Rename函数名为main

Rename函数名为main

CODE XREF: 当前函数被一个或多个地方引用,摁下CTRL+X可以查看调用main()的地方

局部变量汇编分析

.text:00411760 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00411760 main            proc near               ; CODE XREF: j_main↑j
.text:00411760
.text:00411760 var_FC          = qword ptr -0FCh
.text:00411760 var_24          = byte ptr -24h
.text:00411760 var_1D          = byte ptr -1Dh
.text:00411760 var_14          = dword ptr -14h
.text:00411760 var_8           = dword ptr -8
.text:00411760 argc            = dword ptr  8
.text:00411760 argv            = dword ptr  0Ch
.text:00411760 envp            = dword ptr  10h
.text:00411760
.text:00411760                 push    ebp             ; 把调用者函数的EBP压入堆栈
.text:00411761                 mov     ebp, esp        ; 开辟栈帧
.text:00411763                 sub     esp, 0E4h       ; 开辟函数局部变量空间
.text:00411769                 push    ebx             ; 保存易失性寄存器
.text:0041176A                 push    esi
.text:0041176B                 push    edi
.text:0041176C                 lea     edi, [ebp+var_24] ; ebp+var_24 <==> ebp-24h      lea取局部变量地址到edi        EDI = EBP-36 36个字节要初始化作为局部变量的空间
.text:0041176F                 mov     ecx, 9
.text:00411774                 mov     eax, 0CCCCCCCCh
.text:00411779                 rep stosd
.text:0041177B                 mov     ecx, offset unk_41C003
.text:00411780                 call    sub_41130C      ; CheckForDebuggerJustMyCode  VS系统函数
.text:00411785                 mov     [ebp+var_8], 1  ; int nNum = 1
.text:0041178C                 movss   xmm0, ds:dword_417BE8 ; xmm0寄存器用于处理浮点数,2.5存入寄存器
.text:00411794                 movss   [ebp+var_14], xmm0 ; float fNum = 2.5
.text:00411799                 mov     [ebp+var_1D], 41h ; char ch ='A'
.text:0041179D                 movsx   eax, [ebp+var_1D] ; 把ch扩展为4个字节,为压栈做准备
.text:004117A1                 push    eax             ; ch压栈
.text:004117A2                 cvtss2sd xmm0, [ebp+var_14] ; float拓展为double
.text:004117A7                 sub     esp, 8          ; 开辟堆栈空间
.text:004117AA                 movsd   [esp+0FCh+var_FC], xmm0 ; double入栈
.text:004117AF                 mov     ecx, [ebp+var_8]
.text:004117B2                 push    ecx             ; 把nNum的值保存到ecx后入栈
.text:004117B3                 push    offset aIntDFloatFChar ; 格式化字符串后入栈 offset取地址4个字节大小
.text:004117B8                 call    sub_4113A2      ; 调用printf函数
.text:004117BD                 add     esp, 14h        ; 平衡堆栈
.text:004117C0                 xor     eax, eax        ; 把eax清0,等价于 return 0
.text:004117C2                 pop     edi             ; 恢复易失性寄存器
.text:004117C3                 pop     esi
.text:004117C4                 pop     ebx
.text:004117C5                 add     esp, 0E4h       ; 清理main函数开辟的局部变量空间
.text:004117CB                 cmp     ebp, esp
.text:004117CD                 call    sub_411230      ; CheckEsp()   用于在程序的运行时检查堆栈指针是否被篡改
.text:004117D2                 mov     esp, ebp        ; 恢复栈帧
.text:004117D4                 pop     ebp
.text:004117D5                 retn                    ; 将程序的控制流转移到调用它的指令的下一条指令
.text:004117D5 main            endp

main函数主体堆栈,堆栈存储的是函数的局部变量,函数参数

  1. push EBP EBP存储当前函数的栈底地址,调用者函数栈底地址压入堆栈
  2. mov EBP,ESP 开辟栈帧
  3. sub esp, 0xXXX 为函数局部变量开辟空间
  4. push ebx,esi,edi 保存易失性寄存器
  5. 为局部变量空间堆栈初始化为0xCC
  6. 如果main函数有新的局部变量或者调用函数,都会引发堆栈变化
  7. main函数执行完毕开始恢复堆栈
  8. pop 寄存器 恢复易失性寄存器
  9. add esp,0xXX 恢复局部变量的空间
  10. mov esp,ebp ESP=EBP=函数调用者的栈底地址
  11. pop ebp 恢复原先的栈底
  12. retn 将执行main后下一条汇编指令地址给EIP寄存器,恢复程序控制流转,也恢复了原先的栈顶

一些细节:

rep stosd:完成对局部变量的初始化

.text:00411769                 push    ebx             ; 保存易失性寄存器
.text:0041176A                 push    esi
.text:0041176B                 push    edi
.text:0041176C                 lea     edi, [ebp+var_24] ; ebp+var_24 <==> ebp-24h      lea取局部变量地址到edi
.text:0041176F                 mov     ecx, 9
.text:00411774                 mov     eax, 0CCCCCCCCh
.text:00411779                 rep stosd               ; 把9个栈空间初始化为0xCC

汇编:

  1. rep 重复操作,重复操作的次数是ecx的值
  2. stosd:把EAX值复制到EDI寄存器指向的地址,一次复制4个字节
  3. stosw:把EAX值复制到EDI寄存器指向的地址,一个复制2个字节
  4. stosb:把EAX值复制到EDI寄存器指向的地址,一次复制1个字节

之前的操作中,保存易失性寄存器,每个寄存器8个字节,共计24字节

lea edi, [ebp+var_24] ; ebp+var_24 <==> ebp-24h edi寄存器指向main函数局部变量空间的尾地址

下面代码,给局部变量空间初始化

000F176F B9 09 00 00 00       mov         ecx,9  
000F1774 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
000F1779 F3 AB                rep stos    dword ptr es:[edi]

"rep stos dword ptr es:[edi]" 的执行步骤如下:

  1. 使用ES:EDI指定目标内存区域的起始地址。
  2. 使用ECX指定要重复执行的次数。
  3. 将EAX中的值作为DWORD数据写入ES:[EDI]指向的内存位置,然后根据DF(方向标志位)决定EDI是递增还是递减,以定位下一个内存位置。
  4. 重复步骤3,直到ECX达到零,或根据DF决定的方向不再满足条件。

因此,"rep stos dword ptr es:[edi]" 指令将会将EAX寄存器中的DWORD数据连续地写入以ES:[EDI]为起点的目标内存区域,并且重复执行次数由ECX指定。这通常用于快速地内存初始化或者进行内存拷贝操作。

但是在64位下,不再使用ES:EDI模式寻址,64位模式下采用了平坦模型(Flat Model),所有线性地址都直接映射到相同的物理地址空间,即,你可以直接使用RDI寄存器作为指针来操作内存地址,而无需使用ES寄存器进行段寻址。

在x64dbg下验证

DF(Direction Flag)是x86处理器的一个标志位,用于指示字符串操作的方向。

当DF标志位被设置为0时,字符串操作是从低地址向高地址进行的,也就是正向(forward)操作。例如,使用REP MOVS指令将数据从源内存区域复制到目标内存区域时,数据会按照从源地址递增到目标地址的顺序进行复制。

当DF标志位被设置为1时,字符串操作是从高地址向低地址进行的,也就是反向(backward)操作。例如,使用REP MOVS指令将数据从目标内存区域复制到源内存区域时,数据会按照从源地址递减到目标地址的顺序进行复制。

得以验证在64位下,直接可以通过EDI寻址操作,不再使用ES:EDI寻址

为什么要每个字节初始化为0xCC

0xCC表示int33异常0xCC指令是软中断指令,它会导致CPU进入一个中断处理例程。如果程序在调用函数时忘记初始化局部变量,那么在访问这些未初始化的变量时,CPU会立即进入一个中断处理例程,程序会停止运行,并且IDE或调试器会提示有异常产生。

这个地址存储的是浮点数

可以在Edit-->Operand type,选择合适的数据类型,正确的话,就会把16进制数据转换为对应的值。选择floatinteresting point

常见的操作数大小指定符号:

  1. byte ptr:指定一个数据操作数为8位字节(byte)。
  2. word ptr:指定一个数据操作数为16位字(word)。
  3. dword ptr:指定一个数据操作数为32位字 (dword)
  4. mmword ptr是一个指示符,指示使用 64 位内存操作。

下面这两句代码在不同的工具中解析虽然不同,但意思一样。估计IDA 7.0 解析时是按照x86,而VS2019是按照x64,x64中已经不使用DS:EDI寻址了

VS2019:movss xmm0,dword ptr [__real@40200000 (0AF7BE8h)]

IDA:movss xmm0, ds:dword_417BE8

dword ptr [__real@40200000 (0AF7BE8h)] =ds:dword_417BE8 都表示以后面地址为首,取四个字节的数据

  1. xmm0是 XMM 寄存器中的第一个寄存器
  2. 在 x86 指令集中,有 8 个 xmm 寄存器(xmm0~xmm7),每个寄存器的大小为 128 位(16 字节)
  • movss:将一个单精度浮点数(32 位)从源操作数移动到目标操作数。
  • movsx:将源操作数进行符号扩展后移动到目标操作数,源:8位,目标:16位,从低位存,高 8 位设置为源操作数的符号位,不改变大小,拓展位数默认情况下,内存操作数被假定为 32 位的双字(DWORD)数据类型。
  • cvtss2sd:将单精度浮点数(4)转换为双精度浮点数(8)
  • xor:用于执行两个二进制数按位异或操作。当两个对应位的值不同时,结果位为 1,否则为 0。

cmp指令:用于比较两个操作数的大小关系,并根据比较结果设置标志位。cmp指令会将operand1减去operand2的值,并根据结果设置标志位寄存器(如零标志位、符号标志位等)。

  1. 零标志位(ZF):如果两个操作数相等,则 ZF 被设置为 1;否则被设置为 0。
  2. 符号标志位(SF):如果结果为负数,则 SF 被设置为 1;否则被设置为 0。
  3. 位标志位(CF):用于无符号数比较,如果operand1小于operand2,则 CF 被设置为 1;否则被设置为 0。

使用IDA分析全局变量

#include <stdio.h>

int nNum = 1;
float fNum = 2.5;
char ch = 'A';

int main()
{


	printf("int %d, float %f, char %c", nNum, fNum, ch);

	return 0;
}

局部变量汇编分析

.text:00411760 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00411760 main            proc near               ; CODE XREF: j_main↑j
.text:00411760
.text:00411760 var_D8          = qword ptr -0D8h
.text:00411760 argc            = dword ptr  8
.text:00411760 argv            = dword ptr  0Ch
.text:00411760 envp            = dword ptr  10h
.text:00411760
.text:00411760                 push    ebp
.text:00411761                 mov     ebp, esp        ; 开辟栈帧
.text:00411763                 sub     esp, 0C0h       ; 给局部变量开辟空间
.text:00411769                 push    ebx             ; 保存易失性寄存器
.text:0041176A                 push    esi
.text:0041176B                 push    edi
.text:0041176C                 mov     edi, ebp        ; 从EDI从低地址向高地址初始化局部变量空间
.text:0041176E                 xor     ecx, ecx
.text:00411770                 mov     eax, 0CCCCCCCCh
.text:00411775                 rep stosd
.text:00411777                 mov     ecx, offset unk_41C003
.text:0041177C                 call    sub_41130C
.text:00411781                 movsx   eax, byte_41A03C ; 把字符拓展为4字节,压入堆栈
.text:00411788                 push    eax
.text:00411789                 cvtss2sd xmm0, dword_41A038 ; 把float拓展为double
.text:00411791                 sub     esp, 8
.text:00411794                 movsd   [esp+0D8h+var_D8], xmm0 ; 把xmm0压入堆栈 这种压入堆栈方式相当于先抬升,esp变小,在压入xmm,从低地址填充到高地址
.text:00411799                 mov     ecx, dword_41A034
.text:0041179F                 push    ecx
.text:004117A0                 push    offset aIntDFloatFChar ; 压入的是字符串的地址,共计四个字节
.text:004117A5                 call    sub_4113A2
.text:004117AA                 add     esp, 14h        ; ch(4) + xmm0(8) + ecx(4) + 字符串地址(4) = 20字节 = 0x14 平衡堆栈
.text:004117AD                 xor     eax, eax        ; 清0 eax,相当于 return 0
.text:004117AF                 pop     edi             ; 恢复易失性寄存器
.text:004117B0                 pop     esi
.text:004117B1                 pop     ebx
.text:004117B2                 add     esp, 0C0h       ; 恢复函数局部变量空间
.text:004117B8                 cmp     ebp, esp
.text:004117BA                 call    sub_411230      ; CheckEsp检查当前函数栈帧是否被破坏
.text:004117BF                 mov     esp, ebp        ; 恢复栈帧
.text:004117C1                 pop     ebp
.text:004117C2                 retn
.text:004117C2 main            endp

总结

  1. 全局变量保存在data段,在程序编译链接就确定了;局部变量保存在stack段
  2. 访问局部变量往往是通过[ebp-0xXXX],全局变量往往是一个地址[0xXXX]
  3. 把局部变量或者全局变量压入堆栈需要使用寄存器
  4. 给局部变量赋值,mov [esp-0xXXX],值
  5. 函数有返回值往往借助寄存器,值或者地址
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门