前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >int 3中断与软件调试

int 3中断与软件调试

作者头像
py3study
发布2020-01-15 18:08:36
2.4K0
发布2020-01-15 18:08:36
举报
文章被收录于专栏:python3

摘要:平常编程调试的过程中,我们可能会有这样的疑惑:“为什么使用硬件模拟器,比如bochs调试的时候,开始设置的调试断点都不会生效?”,“断点调试的本质是什么,为什么程序能够在特定的地方停留下来?既然程序是指令流,为何CPU没有一直执行下去?”,“在软件中断的情况下,如何进行调试?”。断点和单步执行是两个经常使用的调试功能,也是调试器的核心功能。本章我们将介绍IA-32 CPU是如何支持断点和单步执行功能的,然后逐一为你解答这些疑问。    

1.软件断点  

   x86系列处理器从其第一代产品英特尔8086开始就提供了一条专门用来支持调试的指令,即INT 3。简单地说,这条指令的目的就是使CPU中断(break)到调试器,以供调试者对执行现场进行各种分析。当我们调试程序时,可以在可能有问题的地方插入一条INT 3指令,使CPU执行到这一点时停下来。这便是软件调试中经常用到的断点(breakpoint)功能,因此INT 3指令又被称为断点指令。      下面,我们来测试一个程序:  

代码语言:javascript
复制
1	#include2	#include3	#include4	5	int main()6	{7		printf("hello world");8		__asm__("int $0x03");9		printf("hello world");10		return 0;11	}

 编译:gcc hello.c -o hello    调试:gdb hello   

代码语言:javascript
复制
(gdb) r
Starting program: /home/huangyk/doc/major/操作系统/hello 
Program received signal SIGTRAP, Trace/breakpoint trap.
main () at hello.c:99		printf("hello world");

   可以看到,即使我们不在调试器中设置断点,也能正常中断        查看当前堆栈,定位IP(在64b机器下面,是rip)    

代码语言:javascript
复制
(gdb) info frame
Stack level 0, frame at 0x7fffffffdaa0:
 rip = 0x4004db in main (hello.c:9); saved rip 0x30b3a1ed1d
 source language c.
 Arglist at 0x7fffffffda90, args: 
 Locals at 0x7fffffffda90, Previous frame's sp is 0x7fffffffdaa0
 Saved registers:
  rbp at 0x7fffffffda90, rip at 0x7fffffffda98

 查看内存区域的对应数据  

代码语言:javascript
复制
(gdb) x/10i $rip-20   0x4004c7 :	in     $0xb8,%eax
   0x4004c9 :	clc    
   0x4004ca :	add    $0x89480040,%eax
   0x4004cf :	(bad)  
   0x4004d0 :	mov    $0x0,%eax
   0x4004d5 :	callq  0x4003b8 
   0x4004da :	int3   
=> 0x4004db :	mov    $0x4005f8,%eax
   0x4004e0 :	mov    %rax,%rdi
   0x4004e3 :	mov    $0x0,%eax

     我们可以清楚看见,刚才执行了一个int 3指令。断点异常(INT 3)属于陷阱类异常,当CPU产生异常时,其程序指针是指向导致异常的下一条指令的。        注意:在windows其他的调试器中不是这样,eip被设定成指向int 3指令。    

2.在调试器中设置断点  

   考虑一下调试器是如何设置断点的。当我们在调试器中对代码的某一行设置断点时,调试器会先把这里的本来指令的第一个字节保存起来,然后写入一条INT 3指令。因为INT 3指令的机器码为11001100b(0xCC),仅有一个字节,所以设置和取消断点时也只需要保存和恢复一个字节,这是设计这条指令时须考虑好的。    

代码语言:javascript
复制
(gdb) b 8Breakpoint 1 at 0x4004da: file hello2.c, line 8.
(gdb) r
Starting program: /home/huangyk/doc/major/操作系统/two Breakpoint 1, main () at hello2.c:88		printf("hello world");
Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.132.el6.x86_64
(gdb) x/10i $rip-20
   0x4004c6 :	mov    %esp,%ebp   0x4004c8 :	mov    $0x4005f8,%eax   0x4004cd :	mov    %rax,%rdi   0x4004d0 :	mov    $0x0,%eax   0x4004d5 :	callq  0x4003b8 => 0x4004da :	mov    $0x4005f8,%eax   0x4004df :	mov    %rax,%rdi   0x4004e2 :	mov    $0x0,%eax   0x4004e7 :	callq  0x4003b8 
   0x4004ec :	mov    $0x0,%eax

   你看到了什么?怎么没有int 3指令呢?        值得说明的是,在调试器下,我们是看不到动态替换到程序中的INT 3指令的。大多数调试器的做法是在被调试程序中断到调试器时,会先将所有断点位置被替换为INT 3的指令恢复成原来的指令,然后再把控制权交给用户。    

3.断点命中  

当CPU执行到INT 3指令时,由于INT 3指令的设计目的就是中断到调试器,因此,CPU执行这条指令的过程也就是产生断点异常(breakpoint exception,简称#BP)并转去执行异常处理例程的过程。在跳转到处理例程之前,CPU会保存当前的执行上下文,包括段寄存器、程序指针寄存器等内容。          注意:断点命中之后的中断服务程序是调试器来定义的,然后将服务入口注册到IDT中。    

4.恢复执行  

   这里有一个问题,前面我们说当断点命中中断到调试器时,调试器会把所有断点处的INT 3指令恢复成本来的内容。因此,在用户发出了恢复执行命令后,调试器在通知系统真正恢复程序执行前,调试器需要将断点列表中的所有断点再落实一遍。但是对于刚才命中的这个断点需要特别对待,试想如果把这个断点处的指令也替换为INT 3,那么程序一执行便又触发断点了。但是如果不替换,那么这个断点便没有被落实,程序下次执行到这里时就不会触发断点,而用户并不知道这一点。对于这个问题,大多数调试器的做法都是先单步执行一次。也就是说,先设置单步执行标志(下一节将详细讨论),然后恢复执行,将断点所在位置的指令执行完。因为设置了单步标志,所以,CPU执行完断点位置的这条指令后会立刻再中断到调试器中,这一次调试器不会通知用户,会做一些内部操作后便立刻恢复程序执行,而且将所有的断点都落实(使用INT 3替换)。如果用户在恢复程序执行前,已经取消了当前的断点,那么就不需要先单步执行一次了。    

5.特别用途—— 烫烫烫烫烫  

   因为INT 3指令的特殊性,所以它有一些特别的用途。让我们从一个有趣的现象说起。当我们用VC6进行调试时,常常会观察到一块刚分配的内存或字符串数组里面被填充满了"CC"。如果是在中文环境下,因为0xCCCC恰好是汉字"烫"字的简码,所以会观察到很多"烫烫烫……",而0xCC又正好是INT 3指令的机器码,这是偶然的么?答案是否定的。因为这是编译器故意这样做的。为了辅助调试,编译器在编译调试版本时会用0xCC来填充刚刚分配的缓冲区。这样,如果因为缓冲区或堆栈溢出时程序指针意外指向了这些区域,那么便会因为遇到INT 3指令而马上中断到调试器。    

6.系统对int 3的优待  

   关于INT 3指令还有一点要说明的是,INT 3指令与当n=3时的INT n指令(通常所说的软件中断)并不同。INT n指令对应的机器码是0xCD后跟1字节n值,比如INT 23H 会被编译为0xCD23。与此不同的是,INT 3指令具有独特的单字节机器码0xCC。而且系统会对INT 3指令给予一些特殊的待遇,比如在虚拟8086模式下免受IOPL检查等。    

7.为什么看不到调试期写入的int 3指令  

   因为,调试器总是“执行到b line,替换为int 3指令,调用中断,恢复int 3之前的指令,将现场返回给用户”,所以,int 写入但是又被置换,整个过程对用于是透明的。    

8.归纳与解惑  

   因为使用INT 3指令产生的断点是依靠插入指令和软件中断机制工作的,因此人们习惯把这类断点称为软件断点,软件断点具有如下局限性。        属于代码类断点,即可以让CPU执行到代码段内的某个地址时停下来,不适用于数据段和I/O空间。        对于在ROM(只读存储器)中执行的程序(比如BIOS或其他固件程序),无法动态增加软件断点。因为目标内存是只读的,无法动态写入断点指令。这时就要使用我们后面要介绍的硬件断点。        在中断向量表或中断描述表(IDT)没有准备好或遭到破坏的情况下,这类断点是无法或不能正常工作的,比如系统刚刚启动时或IDT被病毒篡改后,这时只能使用硬件级的调试工具。        虽然软件断点存在以上不足,但因为它使用方便,而且没有数量限制(硬件断点需要寄存器记录断点地址,有数量限制),所以目前仍被广泛应用。        回到我们最开始提出的问题:由于调试是和调试期密切相关的,在用bochs+freedos 来调试操作系统的时候,如果在我们自己的操作系统起来之前,这时候不满足软件中断的使用条件,所以会设置断点失败,需要利用硬件中断,xchg bx,bx;进入到系统之后,然后就可以使用正常的软件中断了。        参考:<软件调试>第四章第一节    http://book.51cto.com/art/200812/100663.htm

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019/06/24 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.软件断点  
  • 2.在调试器中设置断点  
  • 3.断点命中  
  • 4.恢复执行  
  • 5.特别用途—— 烫烫烫烫烫  
  • 6.系统对int 3的优待  
  • 7.为什么看不到调试期写入的int 3指令  
  • 8.归纳与解惑  
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档