Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

《软件调试》 —— 张银奎 #55

Open
thzt opened this issue May 8, 2020 · 0 comments
Open

《软件调试》 —— 张银奎 #55

thzt opened this issue May 8, 2020 · 0 comments

Comments

@thzt
Copy link
Owner

thzt commented May 8, 2020

著名的计算机科学家 Brian Kernighan 曾经说过,软件调试要比编写代码困难一倍,
如果你发挥出了最佳才智编写代码,那么你的智商便不足以调试这个代码。


就像侦破一个案件所需的日期很难确定一样,对于一个软件错误,到底需要多久才能定位到它的根源并解决这个问题,是一个很难回答的问题。
调试任务的难以预测性经常给软件工程带来重大的麻烦,其中最常见的便是导致项目延期。
事实上,很多软件项目的延期是与无法定位和解决存留的 Bug 有关的。

  • 广泛的关联性

很多调试机制是操作系统、中央处理器和调试器相互协作的复杂过程,
比如 Windows 本地调试中的软件断点功能,通常是依赖于 CPU 的断点指令(对于 x86,即 INT 3)的,
CPU 执行到断点指令时中断下来,并以异常的方式报告给操作系统,操作系统再将这个事件分发给调试器。

另外,软件调试与软件编译有着密切的关系。
软件的调试版本包含了很多用来辅助软件调试的信息,具有更好的可调试性。
调试信息中很重要的一个部分,便是调试符号,它是进行源代码级调试所必需的。

综上所述,软件调试与计算机系统的硬件核心(CPU)和软件核心(操作系统)都有着很紧密的耦合关系,
与软件生产的最主要机器 —— 编译器,也息息相关。
因此,可以说软件调试具有广泛的关联性,有时也被称为系统性。


当计算机领域的拓荒者们设计最初的计算机系统时,他们就考虑了调试问题,
包括如何调试系统中的硬件,也包括如何调试系统中的软件。


断点(breakpoint)是使用调试器进行调试的最常用的调试技术之一。
其基本思想是在某一个位置设置一个 “陷阱”,当 CPU 执行到这个位置时便停止执行被调试的程序,
中断到调试器(break into debugger)中,让调试者进行分析和调试。
调试者分析结束后,可以让被调试程序恢复执行。

根据断点的设置方法,可以把断点分为软件断点和硬件断点。

软件断点通常是通过向指定的代码位置插入专用的断点指令来实现的,
比如 IA32 CPU 的 INT 3 指令(机器码为 0xCC)就是断点指令。

硬件断点通常是通过设置 CPU 的调试寄存器来设置的。
IA32 CPU 定义了 8 个调试寄存器:DR0 ~ DR7,可以同时设置最多 4 个硬件断点(对于一个调试会话)。

当中断到调试器时,系统或调试器会将被调试程序的状态保存到一个数据结构中,通常称为执行上下文(context)。
中断到调试器后,被调试程序是处于静止状态的,直到用户输入恢复执行命令。


某一类 CPU 所支持的指令集合被简称为指令集(instruction set)。
根据指令集的特征,可以把 CPU 划分为两大阵营,即 RISC 和 CISC。

RISC(reduced instruction set computer 精简指令集计算机)是 IBM 研究中心的 John Cocke 博士于 1974 年最先提出的。
其基本思想是通过减少指令的数量和简化指令的格式,来优化和提高 CPU 执行指令的效率。
RISC 出现后,人们很自然的把与 RISC 相对的另一类指令集称为 CISC(complex instruction set computer 复杂指令集计算机)。

RISC 处理器的典型代表有 SPARC 处理器、PowerPC 处理器、惠普公司的 PA-RISC 处理器、MIPS 处理器、Alpha 处理器 和 ARM 处理器。

CISC 处理器的典型代表有 x86处理器和 DEC VAX-11 处理器等。


IA-32 处理器是对英特尔设计和生产的 x86 系列 32 位微处理器的总称。
IA-32(或 IA32)是 intel architecture 32-bit 的缩写,用来泛指 IA-32 处理器所使用的架构和共同特征。

IA-32 处理器的典型代表有 80386、80486、Pentium(奔腾)、Pentium Pro、Pentinum II、Pentium III、Pentium 4(简称 P4)、Pentium M、Core Duo、Core 2 Duo 及 Celeron(赛扬)和 Xeon(至强)处理器。

  • CPU 的操作模式
    **保护模式(protected mode):**所有 IA-32 处理器的本位(native)模式,具有强大的虚拟内存支持和完善的任务保护机制,为现代操作系统提供了良好的多任务(multitasking)运行环境。
    **实地址模式(real-address mode):**简称实模式(real mode),即模拟 8086 处理器的工作模式。实模式提供了一种简单的单任务环境,可以直接访问物理内存和 I/O 空间,操作系统和应用软件运行在同一个内存空间和同一个优先级上,因此操作系统的数据很容易被应用软件破坏。DOS 操作系统运行在实模式下,CPU 在上电或复位后总是处于实模式状态。
    **虚拟 8086 模式(virtual-8086 mode):**保护模式下用来执行 8086 任务(程序)的准模式(quasi-operating mode)。通过该模式,可以把 8086 程序当做保护模式的一项任务来执行。实地址模式无疑为运行 8086 程序提供了良好的硬件环境,但由于实地址模式无法运行现代的主流操作系统,从保护模式切换到实模式来运行 8086 程序需要较大的开销,难以实现。虚拟 8086 模式允许在不退出保护模式的情况下,执行 8086 程序,当 CPU 切换到一个 8086 任务时,它便以类似实模式的方式工作,当 CPU 被切换到其他普通 32 位任务时,仍然以正常的方式工作,这样就可以在一个操作系统下 “同时” 运行 8086 任务和普通的 32 位任务了。需要注意的是,运行在虚拟 8086 模式下的 8086 任务在 I/O 访问方面会受到一些限制,与运行在实模式下是有所不同的,但这是为了保证操作系统和其他任务的安全所必需的。
    系统管理模式(system management mode,):供系统固件(firmare)执行电源管理、安全检查或与平台相关的特殊任务。当 CPU 的系统管理中断管脚(SMI#)被激活时,处理器会将当前正在执行的任务的上下文保存起来,然后切换到另一个单独的(seperate)地址空间中执行专门的 SMM 例程。SMM 例程通过 RSM 指令使处理器退出 SMM 模式并恢复到响应系统管理中断前的状态。386 SL 处理器最先引入系统管理模式,其后的所有 IA-32 处理器都支持该模式。
    IA-32e 模式:支持 intel64 的 64 位工作模式,曾经被称为 64 位内存扩展技术(extended memory 64 technology,简称 EM64T),是 IA-32 CPU 支持 64 位的一种扩展技术,具有对现有 32 位程序的良好兼容性。由两个子模式组成:64 位模式和兼容模式。运行在 IA-32e 模式下的 64 位操作系统,系统内核和内核态的驱动程序一定是 64 位的代码,工作在 64 位模式下,应用程序可以是 32 位的(在兼容模式下执行),也可以是 64 位的(在 64 位模式下执行)。

大多数现代操作系统(包括 Windows 9X/NT/XP 和 Linux 等)都是多任务的,CPU 的保护模式是操作系统实现多任务的基础。
保护模式是为实现多任务而设计的,其名称中的 “保护” 就是保护多任务环境中的各个任务的安全。
多任务环境的一个基本问题,就是当多个任务同时运行时,如何保证一个任务不会受到其他任务的破坏,同时也不会破坏其他任务。
也就是要实现多个任务在同一个系统中 “和平共处、互不侵犯”。

所谓任务,从 CPU 层来看就是 CPU 可以独立调度和执行的程序单位。
从 Windows 操作系统的角度来看,一个任务就是一个线程(thread)。

进一步来说,可以把保护模式对任务的保护机制划分为任务内的保护和任务间的保护。
任务内的保护,是指同一任务空间内不同级别的代码不会相互破坏。
任务间的保护,就是指一个任务不会破坏另外一个任务。

任务间的保护是靠内存映射机制(包括段映射和页映射)实现的,
任务内的保护是靠特权级别检查实现的。

任务间保护的主要目的是保护操作系统。
任务内保护的核心思想是权限控制,即为代码和数据根据其重要性指定特权级别,高特权级的代码可以执行和访问低特权级的代码和数据。

低特权级的代码不可以直接执行和访问高特权级的代码和数据。
高特权级通常被赋给重要的数据和可信任的代码,比如操作系统的数据和代码。
低特权级通常被赋给不重要的数据和不信任的代码,比如应用程序。

这样操作系统可以直接访问应用程序的代码和数据,而应用程序虽然可以指向系统的空间,但是不能访问,
一旦访问就会被系统所发现并禁止。

IA-32 处理器定义了 4 个特权级,又称为环(ring),分别用 0、1、2、3 表示。
0 代表的特权级最高,3 代表的特权级最低。
最高的特权级通常是分配给操作系统的内核代码和数据的。

比如 Windows 操作系统的内核模块是在特权级 0(ring 0)运行的,
Windows 下的各种应用程序(例如 MS Word、Excel)等,是在特权级 3 运行的。

因为特权级 0 下运行的通常都是内核模块,所以人们便把特权级 0 运行说成在内核模式(kernel mode)运行。
把在特权级 3 运行说成在用户模式(user mode)运行。
并因此把编写内核模式下执行的程序称为内核模式编程,把为内核摸下编写的驱动程序称为内核模式驱动程序等。


内存是计算机系统的关键资源,程序必须被加载到内存中才可以被 CPU 所执行。
程序运行的过程中,也要使用内存来记录数据和动态的信息。
在一个多任务系统中,每个任务都需要使用内存资源,因此系统需要有一套机制来隔离不同任务所使用的的内存。
要使这种隔离既安全又高效,那么硬件一级的支持是必须的。

CPU 的段机制(segmenation)提供了一种手段可以将系统的内存空间划分为一个个较小的受保护区域,
每个区域称为一个段(segment)。
每个段都有自己的起始地址(基地址)、边界(limit)和访问权限等属性。
实现段机制的一个重要数据结构便是段描述符(segment descriptor)。


从 386 开始,IA-32 处理器开始支持分页机制(paging)。
分页机制的主要目的是高效的利用内存,按页来组织和管理内存空间,把暂时不用的数据放到外部存储器(通常是硬盘)上。
在启用分页机制后,操作系统将线性地址空间划分为固定大小的页面(4KB、2MB 或 4MB)。

每个页面可以被映射到物理内存或存储器上的虚拟内存文件中。

当程序中的指令访问某一地址(虚拟地址)时,CPU 首先会根据段寄存器的内容,将虚拟地址转化为线性地址,
具体过程是根据段寄存器中的段选择子找到段描述符,然后将段描述符中的基地址加上程序中的偏移地址,便得到线性地址。
接下来,如果 CPU 发现包含该线性地址的内存页不在物理内存中,便会产生一个页错误异常(#PF)。
该异常处理程序通常是操作系统的内存管理器例程。

内存管理器得到异常报告后,会根据异常的状态信息,特别是 CR2 寄存器中包含的线性地址,将所需的内存页加载到物理内存中。
然后异常处理例程返回,使处理器重新执行导致页错误异常的指令,这时所需的内存页已经在物理内存中,所以便不会再导致页错误异常了。


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant