分类:
2012-10-22 23:21:26
Linux kernel 0.11版在linux历史上是一个里程碑式的版本,整个kernel代码量10000行左右,十分精巧却五脏俱全,覆盖386保护模式激活,内存分页,软盘硬盘驱动,进程管理,调度,终端,显示,文件系统等模块,建立了一个基本可以正常运转的操作系统,其中最关键的系统引导部分代码一直工作到2.6版才做较大改动,可以说涵盖了整个Linux最精华的部分(尚不包括网络协议部分),是研究Linux kernel的最佳起点。而由于其中的代码基本是Linus独立完成的,历经时代的风雪,其中天才的光芒依然四射。我们置身于一位伟大天才的奋起之处,仿佛在听莫扎特的成名曲,仿佛在读李白的蜀道难,除了叹为观止,顶礼膜拜,实在是无话可说。剑成时,天下已莫能樱其锋。
但由于其中代码相当古老(1991年),很难在新的环境下面编译,运行,给我们的学习造成一定障碍,所以本文研究如何将其移植到目前比较流行的配置上(ubuntu12+gcc4.6),为我们的学习,研究和调试扫清障碍。
一、虚拟机环境
首先,由于Linux0.11是在Minix-386平台上交叉编译得到的,使用的根文件系统也是Minix文件系统,现在要部署这样的平台将会非常麻烦,所以最简便的办法是在虚拟机上进行部署。 我们采用比较流行的Bochs系统来部署。
Bochs是开源软件,目前最新版本是2.6,由于Linux上可用的安装包不带调试环境(bochsdbg),所以我们需要下载Bochs的源码进行编译,按照Bochs的说明文档,这一步还是比较容易搞定的,最终我们得到两个可执行文件,bochs用于正常运行和gdb联调,bochsdbg用于直接启动调试模块。都放在/usr/bin目录下。
然后,从下面的链接下载一个可以在gcc4.0下正常编译和运行的内核版本,这是我们能够找到最接近目前环境的正常版本:
(也有网友在4.6下成功编译和运行Linux0.11),但对代码进行了较多改动,很多地方采用了比较野蛮的方式,不太适合正常使用,具体情况可以参考http://blog.chinaunix.net/uid-23917107-id-3173253.html一文)我们的信念是,既然这套代码曾经正常工作过,就应该不会有太大问题,只是一个和编译器配合的问题,所以当编译或运行不正常的时候,我们需要仔细的检查代码和调试,找到关键点进行修改。
二、编译
编译中发现的错误还是比较容易解决,上述代码包中大多数已经改好了,基本是关于编译选项和新老语法的适应问题(多数与汇编代码有关),新问题大致有下面几个:
1. 代码blk.h 87行#elif语法错误,改为#else 便可
2、make
In file include from init/main.c:9:
include/unistd.h:207: warning: function return types not compatible due to 'volatile'
include/unistd.h:208: warning: function return types not compatible due to 'volatile'
init/main.c:24: error: static declaration of 'fork' follows non-static declaration
init/main.c:26: error: static declaration of 'pause' follows non-static declaration
include/unistd.h:224: note: previous declaration of 'pause' was here
init/main.c:30: error: static declaration of 'sync' follows non-static declaration
include/unistd.h:235: note: previous declaration of 'sync' was here
init/main.c:108: warning: return type of 'main' is not 'int'
make: *** [init/main.o] Error 1
解决办法:
修改 init/main.c 文件
将 static inline _syscall0(int,fork) 修改为 inline _syscall0(int,fork)
static inline _syscall0(int,pause) 修改为 inline _syscall0(int,pause)
static inline _syscall1(int,setup,void *,BIOS) 修改为 inline _syscall1(int,setup,void *,BIOS)
static inline _syscall0(int,sync) 修改为 inline _syscall0(int,sync)
3.继续 make:
tools/build.c: In function 'main':
tools/build.c:75: warning: implicit declaration of function 'MAJOR'
tools/build.c:76: warning: implicit declaration of function 'MINOR'
tmp/ccsMKTAS.o: In function 'main':
build.c:(.text+0xe1): undefined reference to 'MAJOR'
build.c:(.text+0xf7): undefined reference to 'MINOR'
collect2: ld returned 1 exit status
出错原因:'MAJOR' 和 'MINOR' 未定义
解决办法:
在文件 tools/build.c 中添加下面两行,然后删除 #include
#define MAJOR(a) (((unsigned)(a))>>8)
#define MINOR(a) ((a)&0xff)
二、链接
略去不表
三、运行:
运行中系统始终不能正常挂接root文件系统,需要进行调试,结果非常有趣,过程如下:
lamb@lamb-VirtualBox:~/Downloads/linuxgcc$ gdb
GNU gdb (Ubuntu/Linaro 7.4-2012.04-0ubuntu2) 7.4-2012.04
Copyright (C) 2012 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
For bug reporting instructions, please see:
<
0x0000fff0 in sys_link (oldname=0x0, newname=0x0) at namei.c:759
759 iput(dir);
(gdb) b main.c:main
///在main函数入口设置断点:
Breakpoint 1 at 0x677f: file init/main.c, line 109.
(gdb) list
754 return -EACCES;
755 }
756 bh = find_entry(&dir,basename,namelen,&de);
757 if (bh) {
758 brelse(bh);
759 iput(dir);
760 iput(oldinode);
761 return -EEXIST;
762 }
763 bh = add_entry(dir,basename,namelen,&de);
(gdb) c
Continuing.
Stopped due to shared library event
(gdb) list
104 static long main_memory_start = 0;
105
106 struct drive_info { char dummy[32]; } drive_info;
107
108 void main(void) /* This really IS void, no error here. */
109 { /* The startup routine assumes (well, ...) this */
110 /*
111 * Interrupts are still disabled. Do necessary setups, then
112 * enable them
113 */
(gdb) n
114 ROOT_DEV = ORIG_ROOT_DEV;
(gdb) n
115 drive_info = DRIVE_INFO;
//////这两行代码设置root设备,但结果不成功,最终导致root文件系统不能mount
(gdb) disassem
Dump of assembler code for function main:
0x0000677f <+0>: push %ebp
0x00006780 <+1>: mov %esp,%ebp
0x00006782 <+3>: push %edi
0x00006783 <+4>: push %esi
0x00006784 <+5>: push %ebx
0x00006785 <+6>: and $0xfffffff0,%esp
0x00006788 <+9>: sub $0x50,%esp
0x0000678b <+12>: movzwl 0x901fc,%eax
0x00006792 <+19>: mov %eax,0x1ecac
=> 0x00006797 <+24>: mov $0x90080,%eax
0x0000679c <+29>: mov $0x1ee80,%edi
0x000067a1 <+34>: mov $0x8,%ecx
0x000067a6 <+39>: mov %eax,%esi
0x000067a8 <+41>: rep movsl %ds:(%esi),%es:(%edi)
///////上面几行是关键汇编代码,目的是把0x90080处的根设备信息(16字节)拷贝到///////0x1ee80
///////
0x000067aa <+43>: movzwl 0x90002,%eax
0x000067b1 <+50>: shl $0xa,%eax
0x000067b4 <+53>: add $0x100000,%eax
0x000067b9 <+58>: and $0xfffff000,%eax
0x000067be <+63>: mov %eax,0x1cb00
0x000067c3 <+68>: cmp $0x1000000,%eax
0x000067c8 <+73>: jle 0x67d6
0x000067ca <+75>: movl $0x1000000,0x1cb00
0x000067d4 <+85>: jmp 0x67dd
0x000067d6 <+87>: cmp $0xc00000,%eax
0x000067db <+92>: jle 0x67e9
0x000067dd <+94>: movl $0x400000,0x1cb04
0x000067e7 <+104>: jmp 0x6806
0x000067e9 <+106>: cmp $0x600000,%eax
0x000067ee <+111>: jle 0x67fc
0x000067f0 <+113>: movl $0x200000,0x1cb04
0x000067fa <+123>: jmp 0x6806
0x000067fc <+125>: movl $0x100000,0x1cb04
0x00006806 <+135>: mov 0x1cb04,%eax
0x0000680b <+140>: mov %eax,0x1cb08
0x00006810 <+145>: mov 0x1cb00,%edx
0x00006816 <+151>: mov %edx,0x4(%esp)
0x0000681a <+155>: mov %eax,(%esp)
0x0000681d <+158>: call 0x9657
0x00006822 <+163>: call 0x7687
0x00006827 <+168>: call 0x10f46
0x0000682c <+173>: call 0x130dc
0x00006831 <+178>: call 0x12894
0x00006836 <+183>: mov $0x70,%ecx
Quit
(gdb) info r
eax 0x301 769
ecx 0x0 0
edx 0x8e00 36352
ebx 0x3 3
esp 0x1fe30 0x1fe30
ebp 0x1fe8c 0x1fe8c
esi 0xe0000 917504
edi 0xffc 4092
eip 0x6797 0x6797
eflags 0x406 [ PF DF ]
cs 0x8 8
ss 0x10 16
ds 0x10 16
es 0x10 16
fs 0x10 16
gs 0x10 16
(gdb) x /8hu 0x90080
0x90080: 410 16 65280 255 200 0 410 38
/////////////这里是正常的根设备信息
(gdb) x /8hu 0x1ee80
0x1ee80
(gdb) x /8hu 0x1ee70
0x1ee70: 0 0 0 0 0 0 0 0
(gdb) x /8hu 0x90070
0x90070: 64673 15617 0 6005 35630 15646 47105 520
///////在开始拷贝之前,分别记下0x90080-0x90090, 0x1ee80-0x1ee90处的内容,0x1ee70-0x1ee80,和0x90070-90080的内容
0x000067a8 <+41>: rep movsl %ds:(%esi),%es:(%edi)
=> 0x000067aa <+43>: movzwl 0x90002,%eax
0x000067b1 <+50>: shl $0xa,%eax
…………
(gdb) x /8hu 0x1ee80
0x1ee80
(gdb) x /8hu 0x90080
0x90080: 410 16 65280 255 200 0 410 38
///////拷贝完成后,查看0x1ee80处的数据,发现只有地址在0x90080的一个字节被拷贝,
(gdb)
(gdb) x /8hu 0x1ee70
0x1ee70: 64673 15617 0 6005 35630 15646 47105 520
(gdb) x /8hu 0x90070
0x90070: 64673 15617 0 6005 35630 15646 47105 520
///////原来,我们拷贝的16个字节,发生在0x90070-0x90080,而不是我们设想的0x90080-0x90090!
原因就很明显了,执行循环拷贝的时候,一定没有清方向位(cld)!
这里需要对比一下能够正常运行的环境,下面是用gcc1.4编译出的能在Bochs2.2下正常工作的linux0.11:
可以发现在0x666d处,字符串拷贝开始之前,编译器插入cld指令,从而得到正确的结果。为什么新的编译器不插入这条指令呢?这样的改变在编译器文档中一定会做说明,外事不决问谷歌,而谷歌也没有令我失望,我找到了下面这段文字:从gcc4.3开始的,官方文档如下:
GCC no longer places the cld instruction before string operations. Both i386 and x86-64 ABI documents mandate the direction flag to be clear at the entry of a function. It is now invalid to set the flag in asm statement without reseting it afterward.
就是说在string操作之前,编译器不再插入cld指令。因为i386和x86-64的官方文档都明确要求在进入一个函数时要把方向标记(DF)清除,汇编指令设置方向标记(std)而不清除属于非法!
正常情况下,寄存器eflag中DF位是0,表示从低地址到高地址,现在main()函数中置位,一定是在进入main()之前,Linus写的汇编代码使用std设置了方向位而没有恢复。
这段代码在哪里?我们需要查一下源码,由于在main()之前被运行的是startup_32,用来设置中断向量表,页表等,这部分代码在head.s中:
setup_paging:
movl $1024*5,%ecx /* 5 pages - pg_dir+4 page tables */
xorl %eax,%eax
xorl %edi,%edi /* pg_dir is at 0x000 */
cld;rep;stosl
movl $pg0+7,pg_dir /* set present bit/user r/w */
movl $pg1+7,pg_dir+4 /* --------- " " --------- */
movl $pg2+7,pg_dir+8 /* --------- " " --------- */
movl $pg3+7,pg_dir+12 /* --------- " " --------- */
movl $pg3+4092,%edi
movl $0xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) */
std ////////////////////正是这里!!!
1: stosl /* fill pages backwards - more efficient :-) */
subl $0x1000,%eax
jge 1b
xorl %eax,%eax /* pg_dir is at 0x0000 */
movl %eax,%cr3 /* cr3 - page directory start */
movl %cr0,%eax
orl $0x80000000,%eax
movl %eax,%cr0 /* set paging (PG) bit */
ret /* this also flushes prefetch-queue */
正是当年Linus为了代码效率倒序装入页表而用std设置了方向位,然后又没有复位而触发了这个bug。所以修正这个bug,最简单的办法就是在ret之前采用cld复位。
还有一个办法是使用编译器选项,gcc4.3起虽然采用新的处理方式,但为了向上兼容也专门提供了-mcld选项,只要加上这个选项编译文件,就能得到跟老版本一致的结果。说明文档中对这个选项的描述如下:
Starting with GCC 4.3.1, the -mcld option has been added to automatically generate a cld instruction in the prologue of functions that use string instructions. This option is used for backward compatibility on some operating systems and can be enabled by default for 32-bit x86 targets by configuring GCC with the --enable-cld configure option.
这个bug就算处理完了,还有一个地方与此类似,main()函数完成系统初始化后,调用fork()启动init进程,在fork中调用copy_process()复制进程结构,在copy_process中会调用get_free_page()获取空闲内存页,这个函数是用汇编写的,其中使用了std设置方向位:
unsigned long get_free_page(void)
{
register unsigned long __res asm("ax");
__asm__("std ; repne ; scasb\n\t"
"jne 1f\n\t"
"movb $1,1(%%edi)\n\t"
"sall $12,%%ecx\n\t"
"addl %2,%%ecx\n\t"
"movl %%ecx,%%edx\n\t"
"movl $1024,%%ecx\n\t"
"leal 4092(%%edx),%%edi\n\t"
"rep ; stosl\n\t"
"movl %%edx,%%eax\n"
"1:"
:"=a" (__res)
:"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
"D" (mem_map+PAGING_PAGES-1)
);
return __res;
}
可以采用同样的办法改正。
结论:
1. 对于绝大多数情况,编译器比代码可靠,所以遇到问题的时候,首先需要检查的是代码。
2. 对于代码不兼容的情况,编译器多数会提供兼容选项,我们应该尽可能通过修改编译选项解决问题,
轻易不要修改代码,除非明显的错误。