从未初始化的字符串说起

        我们在用C/C++编写程序的时候,尤其是新手,经常犯的错误就是忘记对变量进行初始化。如果一个字符串没有初始化,在调试的时候,会看到如下奇怪的事情

 说到这,让我想起之前在网上看到的一个段子:

        话说,有位仁兄读大学的时候学习C语言课程,有一次他在编写程序的时候忘记给一个字符串赋值,结果在打印字符串的时候,屏幕上出现了一大堆“烫烫烫烫”的字符串,让他大吃一惊。他以为是计算机温度过高,向他发出警报,于是立马关掉计算机,并感慨计算机的智能。

        读到这个段子,也不禁也让我想起,在我初学C的时候,也犯过同样的错误,而且还告诉老师计算机在报警。

那么,未初始化的字符串在调试模式下为什么会显示“烫”呢?我们来说道说道。

一、INT 3

        这里说的可不是一个整形值3,而是一条指令。从8086开始,Intel就在其产品中提供了一条专门用来支持调试的指令INT 3,这条指令的目的就是使CPU中断到调试器,使软件开发者对程序进行各种分析。

        怎么使用呢?很简单,就是在需要调试的地方插入一条INT 3指令,当程序运行到此的时候,就会停下来,比如这样:

int main()
{
	char s[50];
	__asm int 3;
	std::cout << "INT 3" << std::endl;
	return 0;
}

当程序执行到此时,在VS2022中会弹出这样一个窗口提示

我们点击窗口右上角的绿色三角,可以继续执行程序 ,最后程序输出

以上是编译的32位程序,如果是64位,不支持__asm指令的,那怎么办呢?可以用一个函数替代,__debugbreak()

int main()
{
	char s[50];
	__debugbreak();
	std::cout << "INT 3" << std::endl;
	return 0;
}

其实看__debugbreak()的汇编代码会发现,它就是一条int 3指令 

    __debugbreak();
00007FF72D5E24AC  int         3  

这个INT 3指令的机器码为0xCC,而0xCCCC刚好又是汉字“烫”的字符码,这下知道,“烫”是怎么来的了 

二、为什么要使用它设置断点 

        我们经常使用的代码编译器VS,下断点是非常容易的,指定某一行,按F9就下了一个断点,或者在WinDbg中,在成功加载程序调试符号的情况下,也可以使用bp命令设置断点,这个指令似乎用不上。

        就我使用到的场景,一般都是在驱动调试的时候。现在的VS调试驱动比十几年前方便太多了,那个时候,绝大部分都是通过WinDbg与虚拟机进行双机调试(多年前养成的习惯,现在也是喜欢如此调试,因为WinDbg可观察的东西太多了),为了让WinDbg调试器能够中断下来,就会使用这条指令。

        其次,在客户机没有安装VS的时候,要在客户机上调试程序,也可以通过这种形式让程序在可能出问题的地方断下来,然后远程挂上调试器进行调试。

三、INT 3的一些特殊用途

        为什么在调试模式下,对未赋值的字符串或者未初始化的内存赋值为0xCC呢?这其实是编译器故意这样做的。当遇到缓冲区溢出的时候(也就是指针超出了字符串的区域),指针正好指向这些区域,就会触发断点从而中断到调试器,这样开发人员就知道程序哪里出了问题。

        再者,当我们在看某一个函数的汇编代码时,也会发现INT 3的踪影,比如以下汇编代码片段:

int main()
{
00007FF7FC3D2150  push        rbp  
00007FF7FC3D2152  push        rdi  
00007FF7FC3D2153  sub         rsp,158h  
00007FF7FC3D215A  lea         rbp,[rsp+20h]  
00007FF7FC3D215F  lea         rdi,[rsp+20h]  
00007FF7FC3D2164  mov         ecx,1Eh  
00007FF7FC3D2169  mov         eax,0CCCCCCCCh  
00007FF7FC3D216E  rep stos    dword ptr [rdi]  
00007FF7FC3D2170  lea         rcx,[__39878DF1_main@cpp (07FF7FC3E3001h)]  
00007FF7FC3D2177  call        __CheckForDebuggerJustMyCode (07FF7FC3D13DEh)  
	char s[50];
	__debugbreak();
00007FF7FC3D217C  int         3  
	return 0;
00007FF7FC3D217D  xor         eax,eax  
}
00007FF7FC3D217F  mov         edi,eax  
00007FF7FC3D2181  lea         rcx,[rbp-20h]  
00007FF7FC3D2185  lea         rdx,[__xt_z+1B0h (07FF7FC3DAC50h)]  
00007FF7FC3D218C  call        _RTC_CheckStackVars (07FF7FC3D1366h)  
00007FF7FC3D2191  mov         eax,edi  
00007FF7FC3D2193  lea         rsp,[rbp+138h]  
00007FF7FC3D219A  pop         rdi  
00007FF7FC3D219B  pop         rbp  
00007FF7FC3D219C  ret  
00007FF7FC3D219D  int         3  
00007FF7FC3D219E  int         3  
00007FF7FC3D219F  int         3  
00007FF7FC3D21A0  int         3  
00007FF7FC3D21A1  int         3  
00007FF7FC3D21A2  int         3  
00007FF7FC3D21A3  int         3  
00007FF7FC3D21A4  int         3  
00007FF7FC3D21A5  int         3  
00007FF7FC3D21A6  int         3  
00007FF7FC3D21A7  int         3  

当函数结束的时候,会插入大量的int 3指令,这样做有两个目的:其一,来进行函数的内存对齐;其二,防止栈溢出。

四、谈谈INT 3指令的执行过程

        当CPU遇到INT 3指令时, CPU会产生一个断点异常#BP,并转去执行异常处理例程。在跳转之前,CPU会保存当前的上下文,从《Intel IA-32架构开发手册(卷2)》中我们可以找到如下过程

REAL-ADDRESS-MODE:
IF ((DEST ∗ 4) + 3) is not within IDT limit THEN #GP; FI;
IF stack not large enough for a 6-byte return information THEN #SS; FI;
Push (EFLAGS[15:0]);
IF ← 0; (* Clear interrupt flag *)
TF ← 0; (* Clear trap flag *)
AC ← 0; (*Clear AC flag*)
Push(CS);
Push(IP);
(* No error codes are pushed *)
CS ← IDT(Descriptor (vector_number ∗ 4), selector));
EIP ← IDT(Descriptor (vector_number ∗ 4), offset)); (* 16 bit offset AND 0000FFFFH *)
END;

以上是实模式下INT 3指令的执行过程,保护模式下执行过程相对比较复杂,但原理是一样的。

首先,在第2行先检查中断号算出的地址是否超出中断向量表的边界,如果超出,则触发#GP异常。

接着检查是否有至少6字节的来容纳要压入的CS,IP以及EFLAGS低16位的内容,如果不满足,则触发#SS异常。

接着将EFLAGS的IF、TF、AC位清除,并将CS,IP以及EFLAGS低16位压入堆栈后,将注册在IDT中的异常处理例程入口地址加载到CS和IP寄存器中。

接下来CPU就会开始执行异常处理例程函数了。

        一般来说,INT n指令都是执行一条软中断(比如INT 15H等等),INT n指令的机器码一般是0xCD后跟n值,比如上面的INT 15H机器码为0xCD15H,独INT 3机器码是一个字节0xCC,这也是INT 3的一个独特之处。这也使得该指令在某些模式下可以略过某些检查。

写在最后

        这里只是简单提及了一下INT 3指令的作用和执行过程,其实要深入下去,可学的东西还有很多,但大多在程序开发过程中都用不上,被遗忘的可能性也很大,作为兴趣了解倒还不错。

参考资料

《IA-32 Intel® Architecture Software Developer's Manual Volume 2》

《软件调试 (第2版)卷1:硬件基础》张银奎 著

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cantaloupe77

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值