GDB:程序的解剖术

本文是北航《操作系统》课程预习教程的一部分。此版本由本人编写。 2024 年课程实验环境由 GXemul 更换为 QEMU,为了方便同学适应新的实验环境,在预习教程中特地新增《GDB:程序的解剖术》和《QEMU 模拟器介绍》两篇文章。 回想起刚刚踏入编程世界的时候,大概每个人都有这样的经历:仔细编写的程序总是得不到正确的结果,即便将代码从头到尾检查几遍,依旧找不出隐藏其中的错误。虽然我们对自己所写的代码了如指掌,但是代码终究是静态的,无法反映真实的运行情况;虽然各种各样的测试样例可以让我们发现错误,但是程序终归是只有输入输出的黑箱,其中的运行机理让我们束手无策。 为了解决这样的困境你肯定试过很多办法,比如说大名鼎鼎的 “printf” 大法。但是在原有的逻辑中插入没有意义的输出反而会使代码的结构更加混乱,过量的输出同样更加可能掩盖错误的真相,最终离发现错误的目标越来越远。我们需要采用另一种方法,能够在不侵入代码原有逻辑的前提下,追踪程序的运行情况,从而发现程序运行中出现的错误。 GDB 简介 能够实现追踪并控制程序运行功能的程序称为 Debugger,中文称其为调试器。不同语言有着不同的调试器,如 Python 的 PDB、Java 的 JDB。而我们在本篇文章中介绍的则为 GDB,全称为 “GNU Debugger“。 GDB 的吉祥物,一条 “射水鱼”。擅长射出水柱击落岸边植物上的昆虫(Bug)。 GDB 是一个功能十分强大的调试器,它适用于 C、C++、Go、Rust 等多种语言。GDB 最初由 GNU 项目的创始人理查德·马修·斯托曼(Richard Matthew Stallman)编写,并作为 GNU 项目的一部分。根据 GDB 官网的描述,GDB 的主要功能包括: 启动程序并指定可能影响其行为的任何内容。 使程序在指定条件下停止。 当程序停止时,检查发生了什么。 更改程序中的内容,以便可以尝试纠正一个错误的影响,并继续了解另一个错误。 接下来我们会逐步介绍上述功能。看看 GDB 是如何像手术刀一样解剖程序运行的机理,发现病灶所在的。 准备工作 在开始之前需要说明两点: 接下来的内容我们将在 Ubuntu 中进行,这与本课程的实验环境保持一致。同时建议同学们尽量在学习和开发时多多使用 Linux 环境,因为许多项目都只支持 Linux 平台,或只提供 Linux 下的教程和文档。 为了更好地理解 GDB 的指令操作,同学们最好在阅读教程的同时同步进行操作。 实验所提供的跳板机上会安装好所有需要的环境,因此同学们也可以使用跳板机完成本文操作。但是跳板机中会出现由于无法关闭 address space randomization 导致无法设置断点的问题。这一问题可以通过在 GDB 界面中输入 set disable-randomization off 指令解决。...

一月 18, 2024 · 16 分钟 · 3244 字 · Wokron

Linux-Mint双系统的安装及美化

一、前言 我的笔记本现在用起来很慢了。每次听到它嗡嗡的风扇声却又看不到它跑不出来结果的时候,就感觉它好似一头老驴,使劲却又力不从心,腿打着颤却也拉不动身后的货。所以我谋划着新买一台,就让之前的那台好好休息吧。 一想到买台新的笔记本,我的思绪就沿着这条道一直走下去:“要买什么样的配置呢?买了新的电脑要做些什么项目呢?要玩什么游戏呢?” 诸如此类。装一个 linux 系统也是这时产生的想法。有人可能会想,“这有什么用呢?难道 windows 就不能用吗?如果不得不用 linux,wsl 也是很好地办法,或者用虚拟机,甚至直接用 docker,都可以解决。” 确实,如果就满足当下的使用而言,将 linux 作为一个个人使用的真正的系统,相对于 windows 似乎并没有什么优点。 不过呢,我选择折腾这么一阵也并没有什么经过考量的理由,而仅仅是因为自己在主观上更加喜欢 linux 罢了。在我不算太长的接触并学习 linux 的时间里,我从这个系统中感受到了设计的一致性,这是我在更长时间的对 windows 的接触和学习中所没有体会到的。当然,或许在之后看来,我现在的理解也不过是浅薄的认识罢了。但是现在,我还是决定安装一个 linux 系统。 如果有读者的话,希望不要嫌弃我太过啰嗦(笑)。 二、为什么是薄荷 众所周知,linux 是内核,许多不同的组织在 linux 内核的基础上增加了其他必要的软件和应用,开发了不同的发行版。发行版的江湖中帮派林立,主要有三大派系,debian 系、redhat 系和 suse 系。各个派系中又有无数相互关联却又相互区分的发行版。如 debian 的 ubuntu、deepin;redhat 的 fedora、suse 的 opensuse。只需要把这些发行版的大名亮出来,就足以让人眼花缭乱了。 本人也在这些发行版中漂移不定了一段时间,但最终选择了 debian 系的 mint(薄荷)。主要有一下几个原因 debian 系有着 apt 的超级牛力加持,.deb 格式的软件包使用作为广泛 mint 基于 ubuntu,ubuntu 是使用最为广泛的 linux 发行版 mint 精简了 ubuntu 下的一些功能,如 snap;并对初学者较为友好 三、安装操作系统 我新买的电脑是联想拯救者 R9000P,配置如下 设备 配置 处理器 AMD Ryzen 9 7945HX 内存 16G 硬盘 1T 显卡 Nvidia 4060 要安装的操作系统配置...

六月 24, 2023 · 2 分钟 · 386 字 · Wokron

BUAA-OS 实验笔记之 Lab6

一、Lab6 前言 操作系统实验的最后一篇笔记,不说什么了。本文主要讲了 Shell 的实现机制,管道通信略有说明。 二、Shell 程序的启动 这次我们还要回到 Init/init.c 文件。我们的 MOS 的所有实验都结束之后,mips_init 函数应该是这样的 void mips_init() { printk("init.c:\tmips_init() is called\n"); // lab2: mips_detect_memory(); mips_vm_init(); page_init(); // lab3: env_init(); // lab6: ENV_CREATE(user_icode); // This must be the first env! // lab5: ENV_CREATE(fs_serv); // This must be the second env! // lab3: kclock_init(); enable_irq(); while (1) { } } 其中我们使用 ENV_CREATE 创建了两个用户进程。这两个进程的代码在编译时便写入了内核 ELF 文件中。其中第二个进程 fs_serv 就是 Lab5 中用到的文件系统服务进程;而第一个进程 user_icode 则是整个操作系统中除文件系统服务进程外所有进程的共同祖先进程,该进程便用于启动 Shell 进程。user_icode 或为 “user init code” 之意。...

五月 19, 2023 · 16 分钟 · 3311 字 · Wokron

BUAA-OS 实验笔记之 Lab5

一、Lab5 前言 这是最长的一篇文章,可就算这么长,文中出现的代码也不过本次 Lab 中新增加的代码的一小部分。幸好完成本次实验不需要熟悉所有代码,一部分练习甚至不需要熟悉要填写的代码的前后文,只需要根据注释就可以填出很多。可是我感觉本篇文章还是有帮助的,毕竟谁也不知道 Exam 会出什么题。 Lab5 主要分为四部分,分别是镜像制作工具、关于设备的系统调用、文件系统服务进程、文件操作库函数。本文对这四个方面都有所涉及,第二章主要讲镜像制作工具,第三章主要讲文件系统服务进程和文件操作库函数,最后一章讲关于设备的系统调用。 二、磁盘镜像 (1)镜像制作工具 在本次实验中我们要实现一个文件系统。广义来说,一切字节序列都可以称为文件,但本次实验中我们还是主要关注在磁盘中存储的数据,将这些数据按一定的结构组织起来,就是本次实验的主要目标。 本文依旧不按照指导书中的顺序。我们先查看位于 tools 文件夹下的磁盘镜像制作工具 fsformat 的源代码,以便我们理解磁盘以及文件系统的组织结构。 (2)磁盘数据初始化 我们查看 tools/fsformat.c 文件。找到其中的 main 函数。main 函数首先调用了 init_disk 用于初始化磁盘。 int main(int argc, char **argv) { static_assert(sizeof(struct File) == BY2FILE); init_disk(); 该函数中我们要用到一个数据结构 disk。因此我们先考察 disk。disk 是一个数组,大小为 NBLOCK,每个元素是一个结构体,其中有字段 data,是一个 BY2BLK 字节大小的空间,用于存储一个磁盘块的数据。很容易得知,NBLOCK * BY2BLK = 磁盘空间大小。这样就可以理解 disk 起到的作用了,也就是在构筑磁盘镜像时暂时存储磁盘数据,等到构筑完成后再将 disk 中 data 的内容拼接并输出为二进制镜像文件。 struct Block { uint8_t data[BY2BLK]; uint32_t type; } disk[NBLOCK]; 磁盘块是对磁盘空间的逻辑划分;扇区是对磁盘空间的物理划分 另外 Block 结构体还有一个字段 type,该字段的值为如下枚举的值 enum { BLOCK_FREE = 0, BLOCK_BOOT = 1, BLOCK_BMAP = 2, BLOCK_SUPER = 3, BLOCK_DATA = 4, BLOCK_FILE = 5, BLOCK_INDEX = 6, }; 让我们回到 init_disk。该函数中首先将第一个磁盘块类型设为 BLOCK_BOOT,表示主引导扇区。之后我们要从第三个磁盘块开始(为什么不是第二个?因为第二个磁盘块为 “超级块”,将在后面介绍),设置磁盘块的位图分配机制。在函数中我们计算了在磁盘中存储位图需要的磁盘块数量。NBLOCK 是磁盘块的总数,那么我们同样需要 NBLOCK bit 大小的位图,又因为一个磁盘块有 BIT2BLK bit,那么总共需要 NBLOCK / BIT2BLK 个磁盘块。向上取整,总共需要 (NBLOCK + BIT2BLK - 1) / BIT2BLK 个磁盘块来存储位图。现在我们已经将 0 到 nbitblock-1 的位图分配了用途,那么下一个空闲的磁盘块就是 nextbno = 2 + nbitblock 了。...

五月 2, 2023 · 23 分钟 · 4790 字 · Wokron

BUAA-OS 实验笔记之 Lab4

一、Lab4 前言 Lab4 主要实现了系统调用,并通过系统调用实现了进程的创建和通信等操作。按照提示编写代码的难度应该不大(除非你的 Lab3 schedule 函数有 bug,很可惜我就是这样 :(),所以本次的笔记更多的讨论了一些和实验无关的代码。希望不会显得太啰嗦。 二、系统调用 (1)从一个用户程序引入 在之前的几篇文章中,我们大致循着内核初始化的过程进行分析。可是在这 Lab4 中这一思路就不适用了。因为在本次实验中我们所要实现的,不过是一些由内核提供的,可供用户程序调用的接口而已。这种调用被称为系统调用。 但是为了保持文章行文的一致性,我们还是希望确定一个入口开始讲解。正好在 mips_init 中有这样的语句,那我们就从被创建的这个用户程序开始。 // lab4: // ENV_CREATE(user_tltest); 这里需要插一嘴,在 Lab3 中我们就已经使用 ENV_CREATE 完成了一些程序的加载,可你有没有仔细看过被加载的程序的源代码是什么样的?代码在 user/bare 路径下。我们查看其中的 put_a.c 程序 void _start() { for (unsigned i = 0;; ++i) { if ((i & ((1 << 16) - 1)) == 0) { // Requires `e->env_tf.cp0_status &= ~STATUS_KUp;` in kernel to work *(volatile char *)0xb0000000 = 'a'; *(volatile char *)0xb0000000 = ' '; } } } 你会发现这些所谓的程序并没有 main 函数,而是 _start。实际上和内核一样,我们同样在用于用户程序编译的链接器脚本中将程序入口设定为 _start。该脚本为 user/user....

四月 13, 2023 · 18 分钟 · 3782 字 · Wokron

BUAA-OS 实验笔记之 Lab3

一、Lab3 前言 不知道为什么,虽然写 Lab3 所用的时间比 Lab2 少,但这次的笔记居然比 Lab2 长。我认为可能是因为自己在本篇文章中讲了更多和实验本身无关的东西。不过既然讲了,应该也会对进一步认识操作系统起到一些作用吧。希望本篇文章不会显得太啰嗦。 二、内核初始化(再续) Lab2 中,我们在内核初始化阶段初始化了虚拟内存的相关信息,Lab3 中我们要继续这一过程。本次实验中我们会完成进程控制的初始化。 (1)再度 mips_init 我们查看 Lab3 中 init/init.c 的 mips_init 函数的内容变化。与 Lab2 相比,其中多调用了如下的方法 env_init、ENV_CREATE_PRIORITY、kclock_init 和 enable_irq。 void mips_init() { printk("init.c:\tmips_init() is called\n"); // lab2: mips_detect_memory(); mips_vm_init(); page_init(); // lab3: env_init(); // lab3: ENV_CREATE_PRIORITY(user_bare_loop, 1); ENV_CREATE_PRIORITY(user_bare_loop, 2); // lab3: kclock_init(); enable_irq(); while (1) { } } 其中 env_init 用于进程控制的初始化,ENV_CREATE_PRIORITY 手工创建了两个进程,kclock_init 和 enable_irq 设置了时钟中断并启用了中断。后两者将分别在第三和四节介绍。本届只介绍前者。 (2)进程管理的数据结构 让我们深入在 kern/env.c 中的 env_init,在该函数中,首先初始化了两个列表 void env_init(void) { int i; /* Step 1: Initialize 'env_free_list' with 'LIST_INIT' and 'env_sched_list' with * 'TAILQ_INIT'....

三月 30, 2023 · 14 分钟 · 2901 字 · Wokron