您的位置:首页 > 编程语言 > C语言/C++

C++反汇编揭秘2 – VC编译器的运行时错误检查(RTC)

2007-12-18 23:29 507 查看
我在上篇文章举了一个简单的C++程序非常简略的解释C++代码和汇编代码的对应关系,在后面的文章中我将按照不同的Topic来仔细介绍更多相关的细节。虽然我很想一开始的时候就开始直接介绍C++和汇编代码的对应关系,不过由于VC编译器会在代码中插入各种检查,SEH,C++异常等代码,因此我觉得有必要先写一下一些在阅读VC生成的汇编代码的时候常见的一些东西,然后再开始具体的分析C++代码的反汇编。这篇文章会首先涉及到运行时检查(Runtime Checking)

Runtime Checking

运行时检查是VC编译器提供了运行时刻的对程序正确性/安全性的一种动态检查,可以在项目的C++选项中打开Small Type Check和Basic Runtime Checks来启用Runtime Check。

同时,也可以使用/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的时候会报告如下错误:

原因是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或者<128的正数,没有超过范围,直接跳转到00411924获得结果并返回3.     0041190F~00411914:检查eax是否等于0xff00,如果相等,说明这个short值是负数,并且>=-128,在char的表示范围之内,可以接受,跳转到004119244.     如果上面检查都没有通过,说明这个值已经超过了范围,调用_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.     检查数组变量的Overrun3.     检查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个byte2.     接着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,否则出错
      cmp   ebp, esp       call  __RTC_CheckEsp       mov   esp, ebp       pop   ebp       ret   0 ?func@@YAXXZ ENDP
出错的时候显示的对话框如下:

OK,这次就写到这里。下面几篇文章预定会写到下面这些内容:1.     /GS & Security Cookie2.     Calling Conventions3.     Name Mangling4.     Structured Exception Handling5.     Passing by Reference6.     Member functions7.     Object layout8.     Virtual functions9.     Virtual Inheritance10.   C++ Exceptions 11.   Templates敬请关注。 作者:      ATField
Blog:      http://blog.csdn.net/atfield
转载请注明出处 

Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1556844
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: