gdb的基本调试命令

  • r(run) : 开始运行程序;
  • c(continue) : 继续运行一直到断点停止
  • b(break) : 设置程序断点;
  • p(print) : 打印出变量值;如 p var,会把var变量的值输出
  • s(step) : 单步跟踪,会进入函数内部
  • n(next) : 单步跟踪,不进入函数
  • finish : 跳出函数调试,并打印返回时的信息
  • u(until) : 跳出循环体
  • q(quit) : 退出gdb
  • l(list) : 显示当前行后面的源程序
  • bt (backtrace) : 查看堆栈信息
  • info : 查看各类gdb信息以及环境信息,比如:info break 可以查看断点信息
  • clear : 清除全部已定义的断点
  • delete : 删除指点的断点号,后面接断点号
  • gdb -tui main或者在启动gdb后,输入命令focus或layout: 能够在运行时间的同时显示代码

实验环境搭建

下载内核源代码编译内核

cd ~/LinuxKernel/
wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.18.6.tar.xz
xz -d linux-3.18.6.tar.xz
tar -xvf linux-3.18.6.tar
cd linux-3.18.6
make i386_defconfig
make # 一般要编译很长时间

制作根文件系统

cd ~/LinuxKernel/
mkdir rootfs
git clone https://github.com/mengning/menu.git  
cd menu
gcc -o init linktable.c menu.c test.c -m32 -static –lpthread
cd ../rootfs
cp ../menu/init ./
find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img

启动MenuOS系统

cd ~/LinuxKernel/
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img

重新配置编译Linux使之携带调试信息

  • 在原来配置的基础上,make menuconfig选中如下选项重新配置Linux,使之携带调试信息
  • kernel hacking—>
    [*] compile the kernel with debug info
  • make重新编译

使用gdb跟踪调试内核

qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S # 关于-s和-S选项的说明:
# -S freeze CPU at startup (use ’c’ to start execution)
# -s shorthand for -gdb tcp::1234 若不想使用1234端口,则可以使用-gdb tcp:xxxx来取代-s选项

使用gdb调试

gdb
(gdb)file linux-3.18.6/vmlinux # 在gdb界面中targe remote之前加载符号表
(gdb)target remote:1234 # 建立gdb和gdbserver之间的连接,按c 让qemu上的Linux继续运行
(gdb)break start_kernel # 断点的设置可以在target remote之前,也可以在之后

运行启动

调试代码并分析

总的流程图:

首先在start_kernel下断点,运行后,在断点停下:

set_task_stack_end_magic

start_kernel是内核启动阶段的入口,通过单步调试,可以发现它是linux内核执行的第一个init,我们单步进入看看它做了哪些操作:

其中end_of_stack在include/linux/sched.h中,它的意思是获取栈边界地址。然后把栈底地址设置为STACK_END_MAGIC,这个作为栈溢出的标记。STACK_END_MAGIC就是设置在thread_info结构的上面。比如如果你写了一个无限循环,导致栈使用不断增长了,那么,一旦把这个标记未修改了,就导致了栈溢出的错误

local_irq_disable函数

上面两条指令修改了中断寄存器中的IF标志位,sti是中断标志置1指令,使IF=1,cli是中断标志置0指令,使IF=0。所以这里native_irq_disable是关中断,屏蔽中断,接下来的操作就不允许中断对其产生影响。并设置标志位

early_boot_irqs_disabled = true;

page_address_init

初始化高端内存的映射表函数,高端内存是相对于低端内存而存在的,那么先要理解一下低端内存了。在32位的系统里,最多能访问的总内存是4G,其中3G空间给应用程序,而内核只占用1G的空间。因此,内核能映射的内存空间,只有1G大小,但实际上比这个还要小一些,大概是896M,另外128M空间是用来映射高端内存使用的。因此0到896M的内存空间,就叫做低端内存,而高于896M的内存,就叫高端内存了。如果系统是64位系统,当然就没未必要有高端内存存在了,因为64位有足够多的地址空间给内核使用,访问的内存可以达到10G都没有问题。在32位系统里,内核为了访问超过1G的物理内存空间,需要使用高端内存映射表。比如当内核需要读取1G的缓存数据时,就需要分配高端内存来使用,这样才可以管理起来。使用高端内存之后,32位的系统也可以访问达到64G内存。

linux_banner变量保存着linux内核的版本号:

trap_init函数

该函数作用是构建中断描述符表

set_intr_gate(X86_TRAP_DE, divide_error); //除零错误                
set_intr_gate_ist(X86_TRAP_NMI, &nmi, NMI_STACK);//不可屏蔽中断          
 /* int4 can be called from all */                          
set_system_intr_gate(X86_TRAP_OF, &overflow);//溢出              
set_intr_gate(X86_TRAP_BR, bounds);//边界检查错误                        
set_intr_gate(X86_TRAP_UD, invalid_op);//无效指令                               set_intr_gate(X86_TRAP_NM, device_not_available);//无效设备  
……

我们可以参照linux-0.11版本的内核来看这段代码:

Linux-0.11

#set_trap_gate(0,&divide_error)//除零错误
……
#define set_trap_gate(n,addr) \
_set_gate(&idt[n],15,0,addr)//0表示特权级

sched_init函数

对系统的调度机制进行初始化。先是对每个可用CPU上的runqueque进行初始化

然后初始化0号进程

用__sched_fork产生0号进程,并把0号进程的状态设置为TASK_RUNNING,设为系统的idle进程,即系统空闲时占据CPU进程

console_init函数

在窗口输出信息,之前的内存分配信息也打印出来了。

rest_init

这里具体函数分析见上面的流程图,这里主要是fork了一个新进程,并发生进程调度和切换。