[关闭]
@qqiseeu 2013-11-27T12:44:25.000000Z 字数 5370 阅读 3186

调试在64位Debian上编译好的Linux 0.11(一)

本机环境:

编译时的一些设定:

./Makefile

RAMDISK = -DRAMDISK=512  #设定虚拟盘大小为512KB
ROOT_DEV=FLOPPY
LD  =ld -m elf_i386 -Ttext 0 -e startup_32

1.调试前的准备

1.制作内核引导Image

为了把编译好的内核镜像放到bochs中运行,首先制作内核引导Image,具体为(参考Linux内核完全剖析):

  1. #也可以用下述命令替换顶层Makefile中的disk目标下的命令
  2. dd if=Image of=bootimage-fda.img
  3. dd bs=1024 if=/dev/zero of=bootimage-fda.img seek=256 count=1184

其中Image是之前编译出的内核镜像。上述操作得到一个大小为1.44MB的Image文件bootimage-fda.img,将作为在bochs中使用的引导盘。注意此时我们仍未创建根文件系统,而在Makefile中设置了ROOT_DEV=FLOPPY,因此正常情况下在fork出进程1、进程1调用setup((void*)&drive_info)安装根文件系统时,会提示插入含有根文件系统的软盘,不过这暂时不用担心。

2.设置bochs

接下来设置bochs,仍参考Linux内核完全剖析,但由于书中使用的bochs版本较老,有些选项的写法需要修改:

./bochsrc-fda.bxrc

romimage: file=$BXSHARE/BIOS-bochs-latest
megs: 16
vgaromimage: file=$BXSHARE/VGABIOS-elpin-2.40
floppya: 1_44="bootimage-fda.img", status=inserted
floppyb: 1_44=diskb.img, status=inserted
ata0-master: type=disk, path="hdc-0.11-new.img", mode=flat, cylinders=121, heads=16, spt=63
boot: a
log: bochs.out
parport1: enabled=0
vga: update_freq=5
keyboard: paste_delay=100000, serial_delay=200
cpu: count=1, ips=4000000
mouse: enabled=0

注意其中用到的diskb.imghdc-0.11-new.img这个包提供的。其中也包含了做好的根文件系统镜像,可以直接用。然后就可以直接运行系统了!

  1. bochs -f bochsrc-fda.bxrc

3.设置添加了gdb-stub功能的bochs

调试C程序代码时很多时候还是用gdb更为方便,因此可以专门编译出一份开启了gdb-stub功能的bochs,并将其重命名为bochs-gdb以与原来的bochs区分。若要换用开启了“gdb-stub”功能的bochs进行调试,则给所有Makefile文件中的CFLAGS加上 -g 选项,去除所有LDFLAGS中的 -s 选项。此时system模块由于附加了大量调试信息,大小超过了顶层Makefile中设定的限制SYSSIZE = 0x3000,因此可将顶层Makefile中的tools/system目标修改如下:

tools/system:   boot/head.o init/main.o \
                $(ARCHIVES) $(DRIVERS) $(MATH) $(LIBS)
        $(LD) $(LDFLAGS) boot/head.o init/main.o \
        $(ARCHIVES) \
        $(DRIVERS) \
        $(MATH) \
        $(LIBS) \
        -o tools/system > System.map
        objcopy --only-keep-debug tools/system tools/system.dbg
        objcopy --add-gnu-debuglink=tools/system.dbg tools/system
        objcopy -g tools/system

将调试信息抽取出来专门存于一个文件system.dbg中,gdb执行时将会利用该调试信息文件。

2.问题

1.无法正确载入system模块

直接运行内核发现bochs的虚拟终端上不断刷新如下信息

...
ata0 master: Generic 1234 ATA-6 Hard-Disk ( 121 MBytes)
Press F12 for boot menu.
Booting from Floppy...
Loading system ...

初步猜测进入了某个死循环。使用打开了调试功能的bochs单步调试发现,程序执行到setup.s中第193行之前都正常

boot/setup.s

193   jmpi  0, 8  ! jmp offset 0 of segment 8(cs)

这一行的功能是跳转到cs段偏移0处(此时保护模式已打开),实际上就是物理地址0x0处。该地址此时应为system模块的起始处(因在进入保护模式前,setup.s中113-126行的代码已将system模块从线性地址0x10000移至0x0000,而当时线性地址0x0与物理地址0x0是重合的)。然而此时查看物理地址0x0处的内容发现,从该处开始有很长一段(事实上足足有3KB)的值是全零,因此system模块必然出了问题。问题只能出在三个地方:

通过bochs调试前两处可能出现错误的地方,没有发现任何问题,因此问题只可能出在system模块本身。根据bootsect.s中读入system模块的代码,发现其是从第5个磁盘块(每个磁盘块大小为1KB)开始读入system模块的,因此system模块的起始地址应为Image中第2.5KB处,即地址0x0a00,然而使用hexdump查看Image文件发现,system模块实际上是从0x1600字节处开始的,等于说存在一个3KB大小的空洞!build.c中组装system模块的代码如下:

tools/build.c

157    if ((id=open(argv[3],O_RDONLY,0))<0)
158        die("Unable to open 'system'");
159    if (read(id,buf,GCC_HEADER) != GCC_HEADER)
160        die("Unable to read header of 'system'");
161    if (((long *) buf)[6] != 0)
162        die("Non-GCC header of 'system'");
163    for (i=0 ; (c=read(id,buf,sizeof buf))>0 ; i+=c )
164    if (write(1,buf,c)!=c)
165        die("Write call failed");
166    close(id);

可以看出,build程序先读取system模块的GCC_HEADER(实际上就是ELF头)判断文件格式的合法性,然后抛弃该文件头,把剩下的内容添加到Image文件中。这里有一个隐含的假设:system模块的ELF头大小就是1KB!然而实际上,这个用现代版本gcc编译出来的目标文件,其ELF头的大小是4KB(这也可以通过hexdump看出)。因此build程序实际上应该丢弃system模块前4KB的内容。在163行前加上下述代码即可:

  1. /* The header size is 4*GCC_HEADER (4KB) on my machine*/
  2. for (i=0; i<3; i++)
  3. if (read(id,buf,GCC_HEADER) != GCC_HEADER)
  4. die("Unable to read header of 'system'");

2.在显示出“Loading system”信息后就停止运行

按正常情况,应该是在mount_root()函数显示出

Insert root floppy and press ENTER

信息之后系统再暂停运行,然而这里的情况表明程序根本没有执行到这一步。在main.c中的init()调用setup()之前插一句printk("entering init");再重新运行一遍程序,发现根本没有执行到这一行,则有如下几种可能:

首先用bochs调试代码,检查main.c中定义的内联版本的fork()是否被正确展开(其实就是检查main()调用fork()时有没有用call指令。因为这里提到过GCC有时不会把该函数按内联的方式展开)。在我的电脑上编译出来的代码中,这一步是没有问题的,于是接下来深入sys_fork调用去检查。换用bochs-gdb调试代码,结果发现copy_process()中子进程复制父进程task_struct时出现了问题:

*p = *current;

这一步执行完后,并没有把*current复制给*p。查看这两个task_struct结构的周边内存,发现下述情况

...
(gdb) x /16 0x00017140
0x17140 <current>: 0x00017160 0x00000000 0x00000000 0x00000000
0x17150: 0x00000000 0x00000000 0x00000000 0x00000000
0x17160: 0x00000000 0x00000000 0x00000000 0x00000000
0x17170: 0x00000000 0x00000000 0x00000000 0x00000000
(gdb) x /16 0x00ffefe0
0xffefe0: 0x00017160 0x00000000 0x00000000 0x00000000
0xffeff0: 0x00000000 0x00000000 0x00000000 0x00000000
0xfff000: 0x00000000 0x00000000 0x00000000 0x00000000
0xfff010: 0x00000000 0x00000000 0x00000000 0x00000000
...

本次调试时p=0x00fff000current=0x00017160,注意到位于地址0x17140的值似乎被复制到了地址0xffefe0处,这可能说明复制时是往低地址方向复制的。于是在gdb中使用set disassemble-next-line on命令查看copy_process()*p = *current语句产生的汇编代码得:

...
*p = *current; /* NOTE! this doesn't copy the supervisor stack */
=> 0x00007a8a <copy_process+35>:  mov  0x17140,%esi
   0x00007a90 <copy_process+41>:  mov  $0xef,%ecx
   0x00007a95 <copy_process+46>:  mov  %ebx,%edi
   0x00007a97 <copy_process+48>:  rep movsl %ds:(%esi),%es:(%edi)

可见此时使用了rep movsl的方法来复制内存。根据Intel的手册对该指令的描述,其复制方向受EFLAGS寄存器中DF位控制:DF=0时往高地址方向复制(这也是默认情况),DF=1时往低地址方向复制。用info registers查看EFLAGS寄存器的值,果然DF位被置位了。此时编译器认为DF位为0,然而实际上DF=1 。我对于编译原理了解不多,故只能猜测将DF位置位的代码并不是由编译器编译C代码产生的(否则编译器应该会知道自己将DF置位了),因此应是之前某手工编写的汇编代码中存在std指令,且之后没有用cld复位。搜索源文件知

~/Src/LinuxKernel/0.11/linux-0.11-deb$ egrep -nr '\Wstd\W' .
./mm/memory.c:67:     __asm__("std ; repne ; scasb\n\t"
./kernel/chr_drv/console.c:189:     __asm__("std\n\t"
./kernel/chr_drv/console.c:174:     __asm__("std\n\t"
./include/string.h:352:    __asm__("std\n\t"

在这四个使用了std指令的内联汇编代码的最后都加上一句cld复位DF位即可。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注