zoukankan      html  css  js  c++  java
  • 转:C++反汇编揭秘2 – VC编译器的运行时错误检查(RTC)

    作者:      ATField
    Blog:      http://blog.csdn.net/atfield
    转载请注明出处
    本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/ATField/archive/2007/04/08/1556844.aspx

    我在上篇文章举了一个简单的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的表示范围之内,可以接受,跳转到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”)。
    检查ESPESP的错误很有可能是由调用协定的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 Cookie
    2.     Calling Conventions
    3.     Name Mangling
    4.     Structured Exception Handling
    5.     Passing by Reference
    6.     Member functions
    7.     Object layout
    8.     Virtual functions
    9.     Virtual Inheritance
    10.   C++ Exceptions
    11.   Templates
    敬请关注。
     

  • 相关阅读:
    链表和顺序表的区别
    MongoDB安装了以后,服务无法启动的问题
    retrying模块
    Scrapy 动态创建 Item
    pyhon把程序打包为whl
    记录有个关于使用matplotlib库绘图遇到的坑
    读取Polygon多边形的顶点坐标
    Scatter 散点图
    tick 能见度
    Annotation 标注
  • 原文地址:https://www.cnblogs.com/zhyg6516/p/1970482.html
Copyright © 2011-2022 走看看