<iframe align="top" marginwidth="0" marginheight="0" src="http://www.zealware.com/46860.html" frameborder="0" width="468" scrolling="no" height="60"></iframe>
我在上篇文章举了一个简单的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. 在调用其他外部函数的时候:
假设我们有下面的代码:
对应的汇编代码如下:
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,否则出错
pad
分享到:
Global site tag (gtag.js) - Google Analytics
|
相关推荐
描述 "C++反汇编揭秘2 – VC编译器的运行时错误检查(RTC)" 提到了C++编程中的一个重要概念——运行时错误检查(Run-Time Checking, RTC),这是由Visual C++(VC)编译器提供的一项功能。 RTC 是一种在程序运行时...
`/RTC1`开启运行时错误检查,帮助捕获编程错误。 **7. 静态库与动态库** MSVC支持创建静态库(`.lib`)和动态库(`.dll`)。静态库在编译时会被直接包含到目标程序中,而动态库则在运行时被加载。 **8. C++标准支持**...
`RTC`运行时错误检查提供了基本的内存错误检测。`showIncludes`选项列出所有包含的文件,便于跟踪依赖关系。 以上只是部分编译器选项的简要介绍,实际上,Visual C++编译器选项的深度和广度远超于此。理解和熟练...
2. **RTC寄存器配置**:接着,配置RTC的控制和配置寄存器,设置时钟源、预分频因子、闰年规则等参数。这些寄存器包括RTC_CR(控制寄存器)、RTC_TR(时间寄存器)、RTC_DR(日期寄存器)等。 3. **启动RTC**:配置...
RTC,即Real-Time Clock,实时时钟,是一种硬件设备,用于在计算机关闭或不运行时保持精确的时间。它通常由一个电池供电,即使系统电源断开,也能继续工作。RTC在许多领域都有应用,特别是在需要精确时间同步的系统...
RTC,即Real-Time Clock,实时时钟,是嵌入式系统中不可或缺的组成部分,尤其在STM32这类微控制器中,RTC功能对于实现各种时间相关的应用至关重要。STM32的RTC模块可以提供精确的日期和时间信息,即使在MCU主电源...
最后,别忘了在程序运行结束时停用RTC闹钟和中断,以防止不必要的电源消耗。这可以通过`HAL_RTC_DeactivateAlarm()`和`HAL_RTC_DisableIT()`完成。 实验中,你可能还会涉及调试技巧,如使用串口打印RTC时间,或者...
4. 错误处理:当与RTC5卡的通信出现异常时,RTC5Wrap库会抛出相应的异常,帮助开发者快速定位问题。 5. 实时控制:RTC5卡的一大优势是其强大的实时性,可以实现毫秒级别的响应速度。开发者可以利用这个特性进行高速...
RTC是微控制器中用于保持精确时间的硬件模块,即使主电源关闭,也能通过备用电源继续运行。 首先,我们要了解RTC的基本工作原理。RTC通常由晶体振荡器提供精确的时钟源,通过内部电路进行分频,产生秒、分、小时等...
不同的C/C++编译器对于相同的C++源代码会产生不同的机器码(汇编代码)。本文主要探讨Microsoft Visual C++.Net编译器生成的汇编代码。通过对比发现,Visual C++的不同版本之间生成的汇编代码并无显著差异,这在后续...
3. **错误处理**:检测并处理可能出现的硬件故障,确保系统的稳定运行。 4. **通讯协议**:支持多种通讯协议,如RS-232、USB、以太网等,方便与不同类型的控制系统集成。 5. **兼容性**:兼容不同的操作系统,如...
2. **初始化RTC**:在使用RTC前,需要对其进行初始化设置,包括选择时钟源、配置闰年规则、设定初始时间等。这通常通过STM32的HAL库或LL库完成,例如`HAL_RTC_Init()`和`HAL_RTC_SetTime()`函数。 3. **RTC中断**:...
在实际应用中,RTC的测试和验证是非常重要的,因为时间错误可能导致系统运行异常,特别是在那些对时间精度要求高的应用中,如网络同步、定时任务调度等。 总结起来,RTC是电子设备中不可或缺的组件,负责提供精确的...
在编写I2C和RTC相关的汇编代码时,需要注意以下几点: 1. **时序控制**:确保正确地生成I2C时序,包括高低电平的持续时间,以及在适当的时候改变GPIO状态。 2. **错误处理**:I2C通信可能出现应答错误或数据传输...
在遇到错误时,`Rtc_internal.c`的函数可以帮助诊断问题并恢复正常的RTC操作。 总的来说,这个压缩包提供了一套完整的基于I2C的RTC驱动框架,适用于那些需要在嵌入式系统中使用这些特定RTC芯片的项目。开发者可以...
RTC,即Real-Time Clock,是实时钟的缩写,它是一种能够持续计时并保持时间信息的硬件设备,即使在微处理器关闭后也能保持准确的时间。在嵌入式系统,尤其是单片机应用中,RTC常常被用于实现日历和计时功能,如题目...
Keil uVision2是一个流行的嵌入式开发工具,它提供了C/C++编译器、调试器以及项目管理等功能,使得开发者能够在Windows环境下方便地对51单片机进行编程和调试。在Keil uVision2中,用户可以编写RTC相关的源代码,...
RTC4驱动是这个控制器与上位机(通常是运行Windows操作系统的个人电脑)之间的桥梁,允许用户通过计算机对RTC4进行参数设置、控制扫描行为以及获取扫描数据。驱动程序的重要性在于它能够将复杂的硬件指令转换为操作...
RTC6659E与RTC6705是两种在无线通信系统中常见的实时时钟(RTC)芯片,常用于物联网设备、智能家居、移动设备等需要精确时间保持的应用。这两款芯片的数据手册提供了详细的规格参数、功能描述以及应用电路图,帮助...
RTC_TEST.rar_LPC2104 RTC_RTC_RTC_test_arm RTC是一个与嵌入式系统相关的项目,专注于在LPC2104微控制器上实现RTC(实时时钟)功能的测试。LPC2104是NXP公司生产的一款基于ARM7TDMI内核的微控制器,广泛应用于各种...