存档

‘Kernel’ 分类的存档

在 QEMU 上使用 KGDB 调试内核

2011年12月28日 3 条评论

最近在研究 Linux Kernel, 由于我看问题喜欢直接看本质, 所以直接从代码开始看起,
但是 Linux 发展到现在代码何其多, 何其复杂, 里面的流程, 逻辑, 甚至各种变量绝对不是
我以前开发的项目能比的, 比如说里面全局变量的大量使用, 各种 goto 的使用,
所以必须要有一个好的阅读方法和好的阅读手段. 阅读代码的 Emacscscope 足以.
但是对于习惯 gdb 调试的我, 还是希望可以利用 gdb 的强大优势帮助学习. 在加上
QEMU 来作为 kernel 的运行平台, 这样的组合不事半功倍都说不过去.

内核构建

下载内核

经我测试 linux-2.6.24 和 linux-2.6.25 没有 KGDB 的支持. 为了能有 KGDB 的
支持, 我选择版本稍微高一点内核.

# mkdir -p ~/Develop/Linux && cd ~/Develop/Linux
# wget http://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.34.tar.bz2
# tar xf linux-2.6.34.tar.bz2

配置内核

# cd ~/Develop/Linux/linux-2.6.34 
# make defconfig # 用 defconfig 生成 一个精简内核 
# make menuconfig

确保下面的被选中

 General setup  ---> 
     [ * ] Prompt for development and/or incomplete code/drivers
Kernel hacking  --->
     [ * ] Compile the kernel with debug info
     [ * ] Compile the kernel with frame pointers
     [ * ] KGDB: kernel debugger  --->
           < * >   KGDB: use kgdb over the serial console

OK, 下面开始编译内核

# make -j5 # 因为我是 4 核的 CPU, 所以这里使用5个线程同时并发执行

QEMU 构建

安装 QEMU

这里可以选择多种安装方式, 可以选择从源码安装或者从发行版的二进制安装.
作为一个比较喜欢追根究底的 geek, 我选择的是源码安装.

另外, 我调试的只是 kernel, 所以没有硬件虚拟话我是可以忍受的,
所以我把 kvm 从编译参数那里去掉了.

# mkdir -p ~/Develop/QEMU
# cd ~/Develop/QEMU
# wget http://wiki.qemu.org/download/qemu-1.0.tar.gz
# tar xf qemu-1.0.tar.gz
# cd qemu-1.0
# ./configure --prefix=./ --target-list="i386-softmmu x86_64-softmmu" --disable-kvm
# make -j3 && make install

注意, 因为我不想把 qemu 和我系统的 qemu 冲突, 我简单的将 qemu
安装在 qemu 源码目录下.

文件系统构建

安装 busybox

# mkdir ~/Develop/busybox
# wget http://www.busybox.net/downloads/busybox-1.19.3.tar.bz2
# tar xf busybox-1.19.3
# cd busybox-1.19.3
# make menuconfig

静态编译的选择很重要, 如果不选择的话, 需要把 libc.so 和 ld.so 等
复制到文件系统里面, 稍微麻烦一些, 这里我们选择最简单的方式.

另外这个版本静态编译的时候 mount umount会出错, 方正对我来说不需要,
我暂时去掉.

Busybox Settings  ---> 
   Build Options  --->
        [ * ] Build BusyBox as a static binary (no shared libs)
Linux System Utilities  --->
   [ ] mount 
   [ ] umount

执行 make install 后, 会在 busybox 的源码目录地下创建一个 _install
的文件夹, 这个文件夹就是需要复制到文件系统里面的东西.
# make install

制作文件系统

首先创建一个虚拟盘, 并挂载到当前 tmp 目录下.

# cd ~/Develop
# dd if=/dev/zero of=initrd.img bs=1k count=8192
# mkfs.ext3 -F -v -m0 initrd.img
# mkdir tmp
# mount -o loop initrd.img  tmp

然后把编译好的 busybox 复制到挂在虚拟盘的目录里面.

# cp -dpRrf ~/Develop/Linux/busybox-1.19.3//_install/* tmp

创建一些必须的设备文件, 其实设备号几乎是通用的, 所以我直接把本机的设备文件
复制过来了.

# cp -dfrpa /dev/console tmp/dev
# cp -dfrpa /dev/tty* tmp/dev
# cp -dfrpa /dev/mem tmp/dev
# cp -dfrpa /dev/null tmp/dev
# cp -dfrpa /dev/random tmp/dev
# umount tmp

运行

# ~/Develop/QEMU/qemu-1.0/bin/qemu-system-x86_64 \
# -kernel ~/Develop/Linux/linux-2.6.34/arch/x86/boot/bzImage \
# -hda ~/Develop/initrd.img -m 2048 -append "root=/dev/sda init=/bin/sh"

在运行的时候碰到了 init 段错误的问题. 我不知道是不是静态编译导致的.
解决这个问题有两个办法, 或者从其他发行版复制一个静态的 busybox过来,
我试过, 没有问题. 或者把内核启动参数改为 init=/bin/sh 不让 kernel
去启动 init. 我选的是后者.

开始调试

-s 表示用默认的 1234 端口, 开启 gdb server

# ~/Develop/QEMU/qemu-1.0/bin/qemu-system-x86_64 \
# -s -S -kernel ~/Develop/Linux/linux-2.6.34/arch/x86/boot/bzImage \
# -hda ~/Develop/initrd.img -m 2048 -append "root=/dev/sda init=/bin/sh"

在我的 Emacs 里面, 使用 /tmp/gdb/bin/gdb –annotate=3 ~/Develop/Linux/linux-2.6.34/vmlinux
来启动, 进去以后, 设置断点, 然后 target remote localhost:1234 连接 gdb server

其它.

我 gdb 的启动指令是 /tmp/gdb/bin/gdb 而不是默认的 gdb, 因为默认的 gdb 在我的 x86
平台上调试的时候有一个小 bug, 根据邮件列表上的说法.
需要 hack 代码, 然后重新编译. 我打了一个小 patch.

--- gdb-7.3.1-orign/gdb/remote.c    2011-07-15 10:04:29.000000000 +0800
+++ gdb-7.3.1/gdb/remote.c  2011-12-27 18:37:34.319902796 +0800
@@ -5702,9 +5702,21 @@
   buf_len = strlen (rs->buf);
 
   /* Further sanity checks, with knowledge of the architecture.  */
+#if 0
   if (buf_len > 2 * rsa->sizeof_g_packet)
     error (_("Remote 'g' packet reply is too long: %s"), rs->buf);
-
+#endif
+  if (buf_len > 2 * rsa->sizeof_g_packet) {
+     rsa->sizeof_g_packet = buf_len ;
+     for (i = 0; i < gdbarch_num_regs (gdbarch); i++) {
+         if (rsa->regs[i].pnum == -1)
+             continue;
+         if (rsa->regs[i].offset >= rsa->sizeof_g_packet)
+             rsa->regs[i].in_g_packet = 0;
+         else 
+             rsa->regs[i].in_g_packet = 1;
+     }    
+  }
   /* Save the size of the packet sent to us by the target.  It is used
      as a heuristic when determining the max size of packets that the
      target can safely receive.  */

最后献上一张截图: (Emacs 又立功了)

Emacs_KGDB

分类: Kernel, kvm, linux, QEMU 标签: , , , ,

系统引导

2011年10月31日 1 条评论

本文基于 i386 架构分析.

计算机开机通电后 BIOS 将可启动设备(在 BIOS 里由用户设置, U盘, 硬盘, 光盘等)的第一个 扇区的 512 个字节读入内存绝对地址 0x7C00 处, 然后跳转到这个地方执行, 注意此时 CPU 处于 16 位地址的实模式.

远古时代的 Linux 的启动引导

在 boot 目录下共有三个文件 bootsect.S, setup.S, head.S 来做系统开始初始化的一些工作.

bootsect.S

bootsect.S 被编译链接后驻留在启动设备的第一个扇区的前 512 个字节处. 计算机加点启动后 BIOS 将会把 bootsect 加载到 内存 0x7C00 处并开始执行.

这个文件的主要目的是把 setup 和 system(Linux的真正代码)的代码加载到内存中.

bootsect 的执行流程如下:

  1. 用 movw 指令将 0x7C00 开始的 512 字节移动到 0x90000 处, 并跳转到 0x90000 开始执行.
  2. 从磁盘第二扇区开始读取4个扇区到 0x90200 处.
  3. 从磁盘的第扇区开始加载 0x30000(196KB) 的数据到 0x10000 处.
  4. 做完文件系统设备号的检查后, 通过 jmpi 0, SETUPSEG(0x90200) 跳转到 setup 处开始执行.

setup.S

setup 的主要目的是利用 BIOS 提供的中断服务程序(这些中断向量表存放在 0x000开始的位置, 所以 bootsect 加载 system 时候只能从 0x10000 开始) 从设备上提取内核运行所需的机器系统数据, 其中包括光标位置和显示页面, 硬盘参数表等数据, 把它们存放在 0x90000(覆盖 bootsect)开始的位置. 然后移动 system 到 0x000000, 然后设置各种参数, 为 linux 进入 保护模式做好准备.

setup 的执行流程如下:

  1. 将一些系统参数(内存, 硬盘等)存在 0x90000 处.
  2. 关闭中断(cli指令), 将 linux(system模块) 移动到 0x00000, 此时 BIOS 提供 的 16 位的中断机制已经没有了(被覆盖了).
  3. 打开 A20, 实现线性寻址.
  4. 对中断重新编程.
  5. 切换到保护模式.
  6. 跳转到(jmpi 0,8) , 8 = 0x00001000(表示0x00000的特权级, 全局描述符表)

head.S

注意, 此时开始采用 AT&T 的汇编语法,

head 的执行流程如下:

  1. 把相应的段寄存器设置为新的保护模式下的值.
  2. 重新设置中断描述符表(只是简单的初始化, 真正的中断以后再安装)
  3. 重新设置全局描述符表(段限长改为16M)
  4. 确定 A20 线是否开启, 是否含有协处理器.
  5. 以下几步非常关键, 把 main 需要的参数(三个空), L6, 和 main 压栈
  6. 然后设置分页: 开启分页功能. 在内存 0x0 处的 5K 存放一页页目录和四页页表 (此时这四页内核专用, 可寻址 16 M, 页表共用)

现代 Linux 的启动引导

Grub + kernel(): 未完待续

分类: Kernel, linux 标签: ,