我在上篇文章举了一个简单的 C++ 程序非常简略的解释 C++ 代码和汇编代码的对应关系,在后面的文章中我将按照不同的 Topic 来仔细介绍更多相关的细节。虽然我很想一开始的时候就开始直接介绍 C++ 和汇编代码的对应关系,不过由于 VC 编译器会在代码中插入各种检查, SEH , C++ 异常等代码,因此我觉得有必要先写一下一些在阅读 VC 生成的汇编代码的时候常见的一些东西,然后再开始具体的分析 C++ 代码的反汇编。这篇文章会首先涉及到运行时检查( Runtime Checking )
Runtime Checking
运行时检查是 VC 编译器提供了运行时刻的对程序正确性 / 安全性的一种动态检查,可以在项目的 C++ 选项中打开 Small Type Check 和 Basic Runtime Checks 来启用 Runtime Check 。
<shapetype id="_x0000_t75" stroked="f" filled="f" path="m@4@5l@4@11@9@11@9@5xe" o:preferrelative="t" o:spt="75" coordsize="21600,21600"><stroke joinstyle="miter"></stroke><formulas><f eqn="if lineDrawn pixelLineWidth 0"></f><f eqn="sum @0 1 0"></f><f eqn="sum 0 0 @1"></f><f eqn="prod @2 1 2"></f><f eqn="prod @3 21600 pixelWidth"></f><f eqn="prod @3 21600 pixelHeight"></f><f eqn="sum @0 0 1"></f><f eqn="prod @6 1 2"></f><f eqn="prod @7 21600 pixelWidth"></f><f eqn="sum @8 21600 0"></f><f eqn="prod @7 21600 pixelHeight"></f><f eqn="sum @10 21600 0"></f></formulas><path o:connecttype="rect" gradientshapeok="t" o:extrusionok="f"></path><lock aspectratio="t" v:ext="edit"></lock></shapetype><shape id="Picture_x0020_4" style="VISIBILITY: visible; WIDTH: 453pt; HEIGHT: 28.5pt; mso-wrap-style: square" type="#_x0000_t75" o:spid="_x0000_i1027"><imagedata o:title="" src="file:///D:%5Ctmp%5Cmsohtmlclip1%5C01%5Cclip_image001.png"></imagedata></shape>
同时,也可以使用 /RTC 开关来打开检查, /RTC 后面跟 c, u, s 代表启用不同类型的检查。 Smaller Type Check 对应 /RTCc, Basic Runtime Checks 对应 /RTCs 和 /RTCu 。
/RTCc 开关
RTCc 开关可以用来检查在进行类型转换的保证没有不希望的截断( Truncation )发生。以下面的代码为例:
| char ch = 0; 
 
 short s = 0x101; 
 
 ch = s; 
 
 | 
当 VC 执行到 ch = s 的时候会报告如下错误:
<shape id="Picture_x0020_1" style="VISIBILITY: visible; WIDTH: 327pt; HEIGHT: 144.75pt; mso-wrap-style: square" type="#_x0000_t75" o:spid="_x0000_i1026"><imagedata o:title="" src="file:///D:%5Ctmp%5Cmsohtmlclip1%5C01%5Cclip_image003.png"></imagedata></shape>
原因是 0x101 已经超过了 char 的表示范围。
之前会导致错误地的代码对应的汇编代码如下所示:
| ; 42 : char ch = 0; 
 
 
 
 
 mov BYTE PTR _ch$[ebp], 0 
 
 
 
 
 ; 43 : short s = 0x101; 
 
 
 
 
 mov WORD PTR _s$[ebp], 257 ; 00000101H 
 
 
 
 
 ; 44 : ch = s; 
 
 
 
 
 mov cx, WORD PTR _s$[ebp] 
 
 call @_RTC_Check_2_to_1@4 
 
 mov BYTE PTR _ch$[ebp], al 
 
 | 
可以看到,赋值的时候, VC 编译器先将 s 的值放到 cx 寄存器中,然后调用 _RTC_Check_2_to_1@4 函数来检查是否有数据截断的问题,结果放在 al 中,最后将 al 放到 ch 之中。 _RTC_Check_2_to_1@4 顾名思义是检查 2 个 byte 的数据被转换成 1 个 byte 的数据( short 是 2 个 byte , char 是一个 byte ),代码如下:
| _RTC_Check_2_to_1: 
 
 00411900 push ebp 
 
 00411901 mov ebp,esp 
 
 00411903 push ebx 
 
 00411904 mov ebx,ecx 
 
 00411906 mov eax,ebx 
 
 00411908 and eax,0FF00h 
 
 0041190D je _RTC_Check_2_to_1+24h (411924h) 
 
 0041190F cmp eax,0FF00h 
 
 00411914 je _RTC_Check_2_to_1+24h (411924h) 
 
 00411916 mov eax,dword ptr [ebp+4] 
 
 00411919 push 1 
 
 0041191B push eax 
 
 0041191C call _RTC_Failure (411195h) 
 
 00411921 add esp,8 
 
 00411924 mov al,bl 
 
 00411926 pop ebx 
 
 00411927 pop ebp 
 
 00411928 ret 
 
 | 
1. 00411904~00411906 : ecx 保存着 s 的值,然后又被转移到 eax 中。
2. 00411908~0041190D :检查 eax 和 0xff00 相与,并检查是否结果为 0 ,如果结果为 0 ,说明这个 short 值是 0 或者 的正数,没有超过范围,直接跳转到 00411924 获得结果并返回
3. 0041190F~00411914 :检查 eax 是否等于 0xff00 ,如果相等,说明这个 short 值是负数,并且 >=-128 ,在 char 的表示范围之内,可以接受,跳转到 00411924
4. 如果上面检查都没有通过,说明这个值已经超过了范围,调用 _RTC_Failure 函数报错
要解决这个问题,很简单,把代码改为下面这样就可以了:
| char ch = 0; 
 
 short s = 0x101; 
 
 ch = s & 0xff; 
 
 | 
/RTCu 开关
这个开关的作用是打开对未初始化变量的检查,比静态的警告要有用一些。考虑下面的代码:
| int a; 
 
 char ch; 
 
 scanf("%c", &ch); 
 
 
 
 
 if( ch = 'y' ) a = 10; 
 
 
 
 
 printf("%d", a); 
 
 | 
编译器无从通过 Flow Analysis 知道 a 在 printf 之前是否被正确初始化,因为 a = 10 这个分支是由外部条件决定的,所以只有动态的监测方法才可以知道到底程序有没有 Bug (当然从这里我们可以很明显的看出这个程序必然是有 Bug 的)。显然把变量的值和一个具体值来比较是无法知道变量是否被初始化的,所以编译器需要通过一个额外的 BYTE 来跟踪此变量是否被初始化:
函数的开始代码如下:
| push ebp 
 
 mov ebp, esp 
 
 sub esp, 228 ; 000000e4H 
 
 push ebx 
 
 push esi 
 
 push edi 
 
 lea edi, DWORD PTR [ebp-228] 
 
 mov ecx, 57 ; 00000039H 
 
 mov eax, -858993460 ; ccccccccH 
 
 rep stosd 
 
 mov BYTE PTR $T5147[ebp], 0 
 
 | 
最后一句很关键,把 $T5147 变量的值设置为 0 ,表示并没有初始化 a 这个变量。
当 ch = ‘y’ 的时候,编译器除了执行 a=10 之外还会将 $T5147 设置为 1
| mov BYTE PTR $T5147[ebp], 1 
 
 mov DWORD PTR _a$[ebp], 10 ; 0000000aH 
 
 | 
之后,在 printf 之前,编译器会检查 $T5147 这个变量的值,如果为 0 ,说明没有初始化,执行 __RTC_UninitUse 报告错误,否则跳转到相应代码执行 printf 语句:
| cmp BYTE PTR $T5147[ebp], 0 
 
 jne SHORT $LN4@wmain 
 
 push OFFSET $LN5@wmain 
 
 call __RTC_UninitUse 
 
 add esp, 4 
 
 $LN4@wmain: 
 
 mov esi, esp 
 
 mov eax, DWORD PTR _a$[ebp] 
 
 push eax 
 
 push OFFSET ??_C@_02DPKJAMEF@?$CFd?$AA@ 
 
 call DWORD PTR __imp__printf 
 
 add esp, 8 
 
 cmp esi, esp 
 
 call __RTC_CheckEsp 
 
 | 
/RTCs 开关
这个开关是用来检查和 Stack 相关的问题:
1. Debug 模式下把 Stack 上的变量初始化为 0xcc ,检查未初始化的问题
2. 检查数组变量的 Overrun
3. 检查 ESP 是否被毁坏
      
        Debug
        
          模式下初始化变量为
        
        0xcc
        
        
      
    
    假设我们有下面的代码:
| void func() 
 
 { 
 
 int a; 
 
 int b; 
 
 int c; 
 
 } 
 
 | 
对应的汇编代码如下:
| ?func@@YAXXZ PROC ; func, COMDAT 
 
 
 
 
 ; 38 : { 
 
 
 
 
 push ebp 
 
 mov ebp, esp 
 
 sub esp, 228 ; 000000e4H 
 
 push ebx 
 
 push esi 
 
 push edi 
 
 lea edi, DWORD PTR [ebp-228] 
 
 mov ecx, 57 ; 00000039H 
 
 mov eax, -858993460 ; ccccccccH 
 
 rep stosd 
 
 
 
 
 ; 39 : int a; 
 
 ; 40 : int b; 
 
 ; 41 : int c; 
 
 ; 42 : 
 
 ; 43 : } 
 
 
 
 
 pop edi 
 
 pop esi 
 
 pop ebx 
 
 mov esp, ebp 
 
 pop ebp 
 
 ret 0 
 
 ?func@@YAXXZ ENDP 
 
 | 
1. sub esp, 228 : s 编译器为 栈分配了 228 个 byte
2. 接着 3 个 push 指令保存寄存器
3. Lea edi, DWORD PTR [ebp-228] 一直到 repstosd 指令是初始化从 ebp-228 开始写 57 个 0xcccccccc ,也就是 57*4=228 个 0xcc ,正好填满之前 sub esp, 228 所分配的空间。这段代码会把所有的变量初始化为 0xcc 。
选择 0xcc 是有一定理由的 :
1. 0xcc 不同于一般的初始化值,人们一般倾向于把变量初始化为 0, 1, -1 等比较简单的值,而 0xcc 一般情况下足够大,而且是负数,容易引起注意,而且一般变量的值很有可能不允许是 0xcc ,比较容易造成错误
2. 0xcc = int 3 ,如果作为代码执行,则会引发断点异常,比较容易引起注意
      
        检查数组变量的
      
      
        Overrun
        
        
      
    
    假设我们有下面的代码:
| void func 
 
 { 
 
 char buf[104]; 
 
 scanf("%s", buf); 
 
 
 
 
 return 0; 
 
 } 
 
 | 
在 scanf 调用之后,会执行下面的代码:
| mov ecx, ebp 
 
 push eax 
 
 lea edx, DWORD PTR $LN5@wmain 
 
 call @_RTC_CheckStackVars@8 
 
 | 
这段代码会调用 _RTC_CheckStackVars@8 函数会在数组的开始和结束的地方检查 0xcccccccc 有否被破坏,如果是,则报告错误。 _RTC_CheckStackVars 由于代码过长这里就不给出了,这个函数主要是利用编译器保存的数组位置和长度信息,检查数组的开头和结尾:
| $LN5@func: 
 
 DD 1 
 
 DD $LN4@func 
 
 $LN4@func: 
 
 DD -112 ; ffffff90H 
 
 DD 104 ; 00000068H 
 
 DD $LN3@func 
 
 $LN3@func: 
 
 DB 98 ; 00000062H 
 
 DB 117 ; 00000075H 
 
 DB 102 ; 00000066H 
 
 DB 0 
 
 | 
$LN5@func 纪录了数组的个数,而 $LN4@func 保存了数组的偏移量 ebp - 112 和数组的长度 104 ,而 $LN3@func 则保存了变量的名称( 0x62, 0x75, 0x66, 0 = “buf” )。
      
        检查
      
      
        ESP
        
        
      
    
    ESP 的错误很有可能是由调用协定的 mistach 造成,或者 Stack 本身没有平衡。编译器会在调用其他函数和在函数 Prolog 和 Epilog (开始和结束代码)的时候插入对 ESP 的检查:
1. 在调用其他外部函数的时候:
假设我们有下面的代码:
| 
 
 
 printf( "%d", 1 ); 
 
 | 
对应的汇编代码如下:
| mov esi, esp 
 
 push 1 
 
 push OFFSET ??_C@_02DPKJAMEF@?$CFd?$AA@ 
 
 call DWORD PTR __imp__printf 
 
 add esp, 8 
 
 cmp esi, esp 
 
 call __RTC_CheckEsp 
 
 | 
可以看到检查的代码非常简单直接,把 ESP 保存在 ESI 之中,当调用 printf ,平衡堆栈之后,检查 esp 和 esi 的是否一致,然后调用 __RTC_CheckESP , __RTC_CheckESP 代码也很简单:
| _RTC_CheckEsp: 
 
 00412730 jne esperror (412733h) 
 
 00412732 ret 
 
 esperror: 
 
 …… 
 
 00412744 call _RTC_Failure (411195h) 
 
 …… 
 
 00412754 ret 
 
 
 
 
 | 
如果不一致,跳转到 esperror 标号报告错误。
2. 函数返回的时候:
以下面的代码为例:
| void func() 
 
 { 
 
 __asm 
 
 { 
 
 push eax 
 
 } 
 
 } 
 
 | 
Func 函数故意 push eax 来破坏堆栈的平衡性,对应的汇编代码如下:
| ?func@@YAXXZ PROC ; func, COMDAT 
 
 
 
 
 ; 38 : { 
 
 
 
 
 push ebp 
 
 mov ebp, esp 
 
 sub esp, 192 ; 000000c0H 
 
 push ebx 
 
 push esi 
 
 push edi 
 
 lea edi, DWORD PTR [ebp-192] 
 
 mov ecx, 48 ; 00000030H 
 
 mov eax, -858993460 ; ccccccccH 
 
 rep stosd 
 
 
 
 
 ; 39 : __asm 
 
 ; 40 : { 
 
 ; 41 : push eax 
 
 
 
 
 push eax 
 
 
 
 
 ; 42 : } 
 
 ; 43 : } 
 
 
 
 
 pop edi 
 
 pop esi 
 
 pop ebx 
 
 add esp, 192 ; 000000c0H 
 
 cmp ebp, esp 
 
 call __RTC_CheckEsp 
 
 mov esp, ebp 
 
 pop ebp 
 
 ret 0 
 
 ?func@@YAXXZ ENDP 
 
 | 
在函数的初始化代码中, func 会将 ebp 保存在 Stack 中,并且把当前 esp 保存在 ebp 中。
| ?func@@YAXXZ PROC ; func, COMDAT 
 
 push ebp 
 
 mov ebp, esp 
 
 | 
关键的检查代码在后面,当 func 函数恢复了堆栈之后,堆栈会恢复到之前刚保存 esp 到 ebp 的那个状态,这个时候 ebp 必然等于 esp ,否则出错
发表评论
- 浏览: 1292026 次
- 
            性别: 
              
- 来自: 杭州
- 
              
最新评论
- 
            
              netkongjian
            
            : 不错的软件知识,感谢分享!
            
 软件加密方式
- 
            
              norce
            
            : 效果不错~
            
 JS实现图片幻灯片效果
- 
            
              zxbear
            
            : 链接已失效
            
 《jQuery基础教程:第2版》PDF
- 
            
              架构师
            
            : 在技术领域方面Java还是世界上最好的,而且有很多第三方控件的 ...
            
 专访:Ruby能否成为第二个Java
- 
            
              freddie
            
            : 如何拖动表格边框调整行高和列宽?
            
 可编辑的表格(JavaScript)


 
       
       
         
         
         
           
             
					 
					 
		
评论