@qqiseeu
2013-11-27T20:44:25.000000Z
字数 5370
阅读 4207
本机环境:
编译时的一些设定:
./Makefile
RAMDISK = -DRAMDISK=512 #设定虚拟盘大小为512KB
ROOT_DEV=FLOPPY
LD =ld -m elf_i386 -Ttext 0 -e startup_32
为了把编译好的内核镜像放到bochs中运行,首先制作内核引导Image,具体为(参考Linux内核完全剖析):
#也可以用下述命令替换顶层Makefile中的disk目标下的命令
dd if=Image of=bootimage-fda.img
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)
安装根文件系统时,会提示插入含有根文件系统的软盘,不过这暂时不用担心。
接下来设置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.img
与hdc-0.11-new.img
是这个包提供的。其中也包含了做好的根文件系统镜像,可以直接用。然后就可以直接运行系统了!
bochs -f bochsrc-fda.bxrc
调试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执行时将会利用该调试信息文件。
直接运行内核发现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模块必然出了问题。问题只能出在三个地方:
bootsect.s
中109行,将system模块从磁盘上读入内存0x10000处时setup.s
中113-126行,将system模块从0x10000复制到0x00000处时build.c
中157-167行,将system模块组装到Image中时(话说这个我一开始还真没想到)通过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行前加上下述代码即可:
/* The header size is 4*GCC_HEADER (4KB) on my machine*/
for (i=0; i<3; i++)
if (read(id,buf,GCC_HEADER) != GCC_HEADER)
die("Unable to read header of 'system'");
按正常情况,应该是在mount_root()
函数显示出
Insert root floppy and press ENTER
信息之后系统再暂停运行,然而这里的情况表明程序根本没有执行到这一步。在main.c中的init()
调用setup()
之前插一句printk("entering init");
再重新运行一遍程序,发现根本没有执行到这一行,则有如下几种可能:
fork()
调用存在问题首先用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=0x00fff000
,current=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位即可。