分类: LINUX
2020-08-16 21:38:17
李显杰
1、 工作背景
研究linux-0.11已有月余,代码看了好几遍,虽然能看懂,却总感觉如隔靴搔痒一般。这当然是正常的,毕竟不是自己写的东西,毕竟这是个操作系统。但若能亲自修改并调试一番,对其的理解就能更加深刻了。然而在linux-0.11的编译要在装有gcc1.4的系统里进行,那个系统太老了,修改起来也不方便,又没法安装源码阅读器. . . 那能不能在ubuntu下用高版本的gcc编译并运行呢?
2、 工作意义
简单讲下本文所描述工作以及撰写本文的意义吧。
Linux-0.11在高版本gcc-4.6.11下编译调试并能正常运行可以使学习linux-0.11更加方便、容易,也为进一步研究linux2.6奠定基础。更加重要的是,本项工作的过程其实就是一个深入学习的过程,对了解新旧版本gcc之间的差异、linux内核运行机制、linux调试方法有很大帮助。另外,对于linux-0.11在高版本gcc下的编译工作,网上已有不少先驱者,但大都没能让编译过后的os正常运行起来,偶有高人完成,也未能提供足够的调试材料,这让不少初学者对此失去了兴趣。现在有兴趣于此的大哥mm们High起来吧!
当然这项工作在很多人眼里并没多重要,他们认为它一没有科研创新价值,而没有商业盈利价值,还耗费不少时间,他们有更重要的事情去做。这个浮躁的社会里,人们做什么事情总要讲究足够的理由。对此我只能说,你们是没错的。
然我道中人终究还有不少,撰写本文也是希望跟我相同想法的人能从中获得他们需要的帮助,并借此与他们互相勉励一下。有句老话怎么说来着?重要的不是目的地,而是沿途的风景。
当然若有高人前辈路过,愿意批评指正的,望不吝赐教,我将不甚感激涕零。
3、工作目的:
本文工作的目的在ubuntu-11中使用gcc-4.61成功编译linux-0.11源码,并经过调试使之能正常运行。由于时间关系,并未解决系统中所有存在的bug,十分遗憾,若有大哥mm们愿继续完成,我将非常高兴。虽如此,拥有了在这个过程中学到的这些东西,我的工作已有足够的意义。本文的特点是图文并茂,对错误原因解释详细,关键地方有对linux一些机制的阐述,尽量减少读者的理解障碍。
代码包来源:赵炯博士网站中下载bochs安装文件、linux-0.11.tar.Z源码包、linux-0.11-devel-040923.zip源码包(该源码在gcc1.4下编译通过,作为对照使用)。
平台搭建:我使用的环境是VirtualBox中安装ubuntu-11,与win7共享工作文件夹(其中存有linux-0.11源码),win7中使用editplus和source insight进行源码的编辑和阅读。
一、编译过程
对linux-0.11在高版本gcc下的编译过程,网上也有一些散碎材料,赵炯博士的论坛里和《linux内核完全剖析》中也有一部分介绍。本节依据先后顺序将编译过程中每个错误的解决方法依次完整地罗列出来。
1、除错工具:
(1)sourceInsight,源码阅读器肯定需要了,否则岂不是找崩溃吗?
(2)internet,编译过程网上有不少人完成了,赵炯博士也完成了,对于一些问题可以查到相应资料。
(3)editplus,这是个编辑器了,我用之多年,比较喜欢,其特点就是小但功能全。
2、问题及解决方法
问题1:
第一次make会出现如图1.1所示的问题。
图1.1
这是注释方式问题 ,as86不支持/**/,改为每行前面用!注释。
问题2:
再次make后出现如图1.2所示的错误。
图1.2
修改所有makefile文件的编译器修改一下,将gas,gld改成as和ld,所有AS的选项参数去掉-c(as不用这个选项),gcc的选项参数去掉-fcombine-regs -mstring-insns(这两个参数现在的gcc不支持了)。
问题3:
接着又出现如图1.3所示的编译错误。
图1.3
这是新旧版本gcc对齐的表达方式不同所造成的,.align n n改为 2n便可。
问题4:
接着又出现如图1.4所示的编译错误。
图1.4
上面的错误只有static declaration of ‘pause’ follows non-static declaration这类是必须修改的,查看main.c和unistd.h发现产生错误的原因:
Main.c中有如图1.5所示的语句:
图1.5
而文件中有图1.6所示的声明语句:
图1.6
Main.c头部包含了unistd.h,那么关于fork、pause、sync系统调用的声明就重复了,不知道为什么旧版本的gcc可以编译通过,需要详查,在main.c中增加这些函数的内联声明的原因是linux-0.11的进程0和1共用一个栈,进程0在创建了进程1后只有等待pause的功能,为了避免进程1栈中数据被破坏, 进程0不可使用栈,那么就不可使用call来调用fork和pause库函数,而应该用内联的方法,防止参数传递将栈搞乱。
修改这个问题的方法可以利用条件宏的预处理作用来进行。在main.c开始包含#define _IN_MAIN,而unistd.h中的声明做如图1.7所示修改即可。
图1.7
问题5:
Make后又发现如图1.8所示的错误。
图1.8
这是编译器对嵌入式汇编已用过的寄存器声明无效的报错信息。查看源文件发现问题出现在_set_base宏这里。
在 include/linux/sched.h中,有如下语句:
188 #define (addr,base) \
189 __asm__("movw %%dx,%0\n\t" \
190 "rorl $16,%%edx\n\t" \
191 "movb %%dl,%1\n\t" \
192 "movb %%dh,%2" \
193 ::"m" (*((addr)+2)), \
194 "m" (*((addr)+4)), \
195 "m" (*((addr)+7)), \
196 "d" (base) \
197 :"dx") //无效声明
去掉197行中的:"dx"这项。也即197行变成了:
197 )
然后去掉所有汇编中的类似的地方:String.h,memory.c,buffer.c,super.c(原因是Bitmap.c)。
这是由于as不断改进,目前自动化程度越来越高,不用人工指定一个变量需要的寄存器了,所以代码中的 __asm__("ax")需要全部去掉。如bitmap.c和namei.c。
问题6:
Make后又发现如图1.9所示的错误。
图1.9
查看segment.h 有如图1.10所示代码。
图1.10
将r改为q,该问题就解决了。r表示使用任意动态分配的寄存器,q表示使用动态分配字节可寻址寄存器(eax,ebx,ecx或edx)。
问题7:
Make后又发现如图1.11所示的错误。
图1.11
这是个左值问题,查看exec.c源码如图1.12所示:
图1.12
这个写法确实有些别扭,使用get_free_page()申请新页面,页面指针pag指向新页面,申请不到空闲页面则返回0。如图1.13修改便可。
图1.13
类似问题在malloc.c中也有。
问题8:
Make后又发现如图1.14所示的错误。
图1.14
查看代码blk.h发现是木有判断的逻辑表达式,改为#elif 1便可。
问题9:
Make后又发现如图1.15所示的错误。
图1.15
这个问题不懂怎么改,按照之间类似问题将第三个冒号后面的寄存器删掉也不行,求指点。这里我果断把所有汇编注释掉,因为strtok这个函数在内核中没有被调用。
至此,编译问题全部解决。
二、链接过程
编译通过了,但请别高兴太早,因为链接过程中遇到的问题一点也不少。
1、除错工具:
(1)sourceInsight,源码阅读器肯定需要了,否则岂不是找崩溃吗?
(2)internet,链接过程网上有不少人完成了,赵炯博士也完成了,对于一些问题可以查到相应资料。
(3)editplus,这是个编辑器了,我用之多年,比较喜欢,其特点就是小但功能全。
2、问题及解决方法
问题1:
链接错误中,程序入口问题首当其冲。错误信息如图2.1所示。
图2.1
这个链接问题的原因就是链接器在将一堆目标文件组合到一起的时候,不知道那个应该放在开头,也就是生成的可执行文件的第一条指令应该是哪个目标文件的第一条。根据内核知识可知,内核最先运行的是head程序,所以打开head.s在.text段中增加.globl start_32。然后在ld的参数选项中增加-m elf_i386 -Ttext 0 -e startup_32。
问题2:
之后又出现如图2.2所示的问题。
图2.2
这个链接问题是由于找不到变量所致,原因是早期编译linux-0.11的汇编器需要在c变量之前增加下划线来识别,而目前的汇编器已足够强大,不用下划线了,所以要全部去掉,这是个枯燥的工程啊。
问题3:
然后又出现如图2.3所示的问题。
图2.3
这种问题是由于没有这两个串口中断处理程序,可以定义一个空程序,或者直接去掉初始化内容
问题4:
之后又出现如图2.4所示的问题。
图2.4
这个问题比较有意思,对keyboard.S做一些处理后生成keyboard.s,但是gcc-4.6.1不区分文件名大小写,故而源文件和目标文件成了同一个。解决方法很简单,将源文件keyboard.S改名为keyboard1.S,同时修改其makefile便可。
问题5:
接着又出现如图2.5所示的问题。
图2.5
在所有错误涉及到的目录下(这里是kernel/,fs/,kernel/chr_drv/)的Makefile里找到CFLAGS然后添加-fno-stack-protector标志!其实这是传给GCC的一个编译选项。-fno-stack-protector参数用来disable Stack-smashing protection ,高版本gcc默认用-fstack-protector参数进行编译。
问题6:
之后又出现如图2.6所示的问题。
图2.6
这个问题简单,就是build.c程序找不到MAJOR和MINOR的实现体,那么就打开build.c将这两个宏的实现加入便可,如图2.7所示。
图2.7
至此,make已经可以通过了。
三、调试过程
编译链接成功后,运行如图3.2所示。大多数linux-0.11的玩家都停到这里了,但是咱不是得做小部分人嘛,那继续吧。
1、调试方法与工具:
(1)插桩(printf、while(1)()等手段)。注意 Printk在内核态使用,比如中断处理函数,系统调用处理函数等,而printf在用户态使用,比如init进程体里面。
(2)bochs单步(s、n)、断点(b、blist、del、c)、内存检测(x、watch)等调试手段。
(3)make时自动生成的可执行文件链接图system.map,如图3.1所示,LDFLAGS中的-M选项实现此功能。这个文件在调试内核时相当重要,它展示了每个全局变量、全局可见的函数的地址,该文件配合bochs的断点和内存查看、监控命令,几乎无所不能。
(4)objdump生成的反汇编文件(如图3.1红线标注部分所示修改makefile,然后make DISASM便可生成该文件)调试时可以用来确认汇编码所属的函数,也可用来阅读汇编码。
图
(5)赵炯博士oldlinux网站上下载的linux-0.11-devel-040923包,可获得用gcc1.4编译后可正常运行linux-0.11。调试时我们用gcc-4.6.1编译的内核运行情况和正确的对比,有确认错误的功能。
(6)hexdump –C rootimage-0.11 |less命令可用来查看文件系统数据,并确认其正确性,调试过程涉及到execve系统调用后,每次运行系统都需重新更换文件系统软盘镜像,因为上次运行可能修改了其中的数据。
2、问题及解决方法
问题1:
图3.2
首先需要确定的是图3.2所示的运行到底停在了那里。内核刚开始正式运行时进入了main函数,在这里进入第0个进程后,创建出第一个进程运行init函数,如图3.3所示。插桩输出是最普通也常常是最有效的调试方法。在init函数中使用printf发现并没有输出,说明系统没有进入第一个进程,那么fork系统调用(功能:创建新进程)就有问题。使用bochs调试功能查看main函数汇编源码,发现调用fork时使用了call指令而非应有的内联。
图3.3
linux-0.11的进程0和1共用一个栈,进程0在创建了进程1后只有等待pause的功能,为了避免进程1栈中数据被破坏, 进程0不可使用栈,那么就不可使用call来调用fork和pause库函数,而应该用内联的方法,防止参数传递将栈搞乱。为了实现这一点,在main.c开始为几个系统调用出了内联声明,如图3.4所示,static inline表示仅仅在该文件中这些系统调用时内联的。
图3.4
然而通过调试可知,该内联功能并未实现(具体原因不详,求高人指点),以至于进程1栈混乱。但仍可以做如图3.5所示修改使之运行通过,其实就是直接将这两个库函数的实现拷入main.c中。
图3.5
问题2:
经过上述修改后,编译运行发现系统运行状况依然如故。天呐,求你动一动吧. . .
说明fork系统调用还有其他问题。Bochs在fork软中断的处理函数copy_process中设断点,但不执行并使用x /nwx addr命令验证每一步的操作是否成功。发现*p=*current并没有将进程0的进程描述体赋值给进程p地址指向的内存,而是赋给了以p地址为末尾、以sizeof(task_struct)为大小的一段内存。那么问题明显了,这句c语句一定是用了rep指令,df位为向下拷贝(之前某个地方使用了std指令)。而新编译器编译c代码时,会认为已经cld过了,因此没加修改位的指令。所以在所有c语言汇编后使用了rep指令拷贝数据的地方(使用objdump生成的反汇编文件中,搜索rep),都加上cld内嵌汇编。其实只有copy_process一个,其他用到rep的地方本来就是汇编,拷贝前都有cld或者std。做如图3.6所示修改后,编译运行如图3.7所示。
图3.6
图3.7
问题3
这个问题提示信息比较直接一些,就是文件系统无法读取。那肯定是软盘或者硬盘根设备没有配置好了。为了简单,本文且使用软盘装着文件系统。需要配置以下几个地方。
(1)bochsrc-hd.bxrc中配置,使用软盘驱动器b来装载文件系统盘。
floppya: 1_44="Image", status=inserted #内核映像
floppyb: 1_44=rootimage-0.11, status=inserted #软盘镜像及其文件系统,该软盘从赵炯博士的oldlinux网站上下载的linux-0.11-devel-040923压缩包中可以找到,当然也可以自己制作。
(2)修改Image的生成工具----build。在tool目录下打开build.c,做如图3.8所示修改,将设备号设置为021d。
图3.8
(3)在引导程序bootsect.S中修改ROOT_DEV常量的值为根设备号,如图3.9所示。
图3.9
内核中的ROOT_DEV不用修改或者复制,因为在系统初始化时(main函数开始),使用ORIG_ROOT_DEV初始化了该变量,而ORIG_ROOT_DEV指向bootsect中INITSEG(初始化段)中的内存单元,在引导程序中已被赋值成021d。
完成上述配置后,从linux-0.11-devel-040923中拷贝rootimage-0.11软盘文件系统到本编译的根目录下,make、运行,结果如图3.10所示。
图3.10
问题4:
这个问题也比较清晰,在source insight中搜索Bad data_limit的引用便可以查到是在copy_mem中的出错信息,可见fork调用仍然创建进程失败。copy_mem()在fork.c中由copy_process()调用。如图3.11所示,copy_mem首先用get_base函数取进程0的代码段和数据段基址,检查是否相等(早期的linux二者是相等的),然后就出错了。
图3.11
Bochs调试跟踪并查看copy_mem函数汇编如图3.12所示。每个红色括号包括一个get_base内联函数的汇编码。显而易见,两个get_base调用的汇编语句数量居然不同,而且后者直接使用了前者生成的edx数据,对于两个独立的函数调用这当然是不允许的。后一个get_base必须重新给edx赋值。
图3.12
如图3.13所示,增加下划线标注的语句,每次调用先保存和恢复edx的值。
图3.13
问题5
十分遗憾,这个问题的运行图没留下来,因为那几天太忙了,总想赶快调完,结果欲速却不达。好了,进行上述修改后,init进程可以运行起来了,但却显示打不开文件,查看init代码可知,现在无法打开/dev/tty0。到底open系统调用哪里出现了问题呢?先看看open系统调用的函数调用关系:sys_open->open_namei->dir_namei->get_dir。使用bochs调试功能一步一步进行跟踪,发现get_dir中使用get_fs_byte无法获取路径字符串的值。
get_fs_byte的作用是使用fs中的值作为数据段选择子来访问内存地址一个字节数据。进程1的局部变量” /dev/tty0”的首地址addr一定保存在该进程的栈里,若在进程1中访问该变量应该是这种形式:ds:addr(ds就是数据段的意思,根据ds可得到数据段选择子,从而得到数据段基地址,加上偏移地址addr便是线性地址,linux-0.11内存完全分页,故而已0为基地址的数据段其局部变量的线性地址==物理地址)。在进程内部ds、fs的值都是该进程数据段的首地址,而若在内核态访问该进程的局部变量就不应该使用ds(包含内核态数据段选择子)了,而应该用:fs: addr(fs指向的数据段的基地址+addr就是线性地址),因为linux-0.11为了能在内核态访问用户态的数据,由用户态在进入内核态时没有修改fs的值,即fs仍然包含用户态某一进程的数据段选择子。
因此要特别注意不能在get_dir中直接使用printk输出pathname来验证函数传参的正确性,因为printk访问pathname肯定是以ds作为数据段的,而pathname指向的变量在fs(用户态)数据段中。而且这里还有一个特别迷惑性的状况:使用printk输出”/dev/tty0”字符串是成功的,让人误以为这里没问题,其实大错特错,因为这只是一种特殊的情况。由于编译器在编译时,把所有它能看见的代码放到一起,并以0为基地址为每个函数局部变量分配地址addr。编译器不知道init函数是一个数据段基地址非0的进程体,因此不论在内核态还是用户态,以0为数据段基地址,addr为偏移地址肯定能访问得到想要的局部变量。因此这里get_dir函数虽然在内核态,还是可以通过ds:addr访问得到pathname的值。而进程1的数据段基地址是0x4000000(每个进程64M虚拟空间),在fork创建的时候共享了父进程的页表,故而0x4000000+addr同样可以访问该字符串的值,而且这种方式才是正确的方式,get_fs_byte使用的就是这种方式。总之,这里无论是用0+addr(ds:addr)还是0x4000000+addr(fs:addr)都应该可以访问到该字符串。如果进程的代码不是编译器可见的,比如execeve之后从文件系统加载的shell可执行文件,那么它的局部变量就不能从以0为基地址的数据段访问(编译器未为其局部变量分配空间),只能用该进程数据段的基地址加偏移来访问了,这时,内核态就无法使用printk输出pathname了。这一点曾经差点纠结死我。
还要注意,在linux-0.11中倘若进程的数据段以0为基地址,那么其局部变量的线性地址==物理地址,因为linux分页管理是从0开始完全分页的(学习内存管理时,理解这一点很重要)。
根据get_dir中使用get_fs_byte无法获取路径字符串的值这一现象,我用x命令查看了fs选择子所指向的数据段描述符,从中提取的基地址却不是0x4000000,那么这就是原因所在了。一定是fork创建进程1时,在copy_mem()中没能给新进程设置正确的数据段基地址,查看copy_mem()汇编码如图3.14所示,红色括号标注的是该函数连续两次调用的set_base函数,为数据段和代码段设置基地址。很明显先后两次调用的函数体汇编码数量却不一样,而且第二次调用直接使用了第一次使用的edx数值,跟问题4一样,这是不允许的。第二次调用之前一定要恢复edx第一次调用修改之前的数值。
图3.14
因此增加如图3.15所标注的指令。edx之前循环右移16位,函数功能完成后,再循环右移16位,恢复原值。
图3.15
编译,运行后,情况如图3.16所示,新问题出现了。
图3.16
6:
这个问题提示信息很不明确,调试也相对麻烦一些。
根据单步、断点等一顿乱调,最后确定出错的位置是do_execve()中两个调用出现了问题,源码如图3.17所示。(请容我稍微complaint一下:看看do_execve那近两百行的c代码,想想它可能编译出的汇编码数量,你也许可以image一下我定位这个错误费了多大周章,因为这种调试最悲催的一点就是即便用插桩得到了大致位置,也得从函数头单步走过去详细查看,除非你愿意花大量时间将汇编和c代码一一对应起来…)。
当我们单步运行第一个free_page_table时,发现仍旧是get_base()函数出了问题。
图3.17
分析eflags寄存器的值,其溢出位为1,即发生了溢出错误。(注意:linux-0.11被新编译器编译后,错误提示并不正常,提示是divide异常,但eflags确实溢出错误。这些错误我们姑且放任,先让其能正常运行起来)。
经过单步调试后发现,还是在上述free_page_tables调用的get_base内部发生了溢出。查看溢出时的处理器信息如图3.18所示, edx左移16位时,eflags的值由0x207变成0xa07, edx溢出了,说明左移时edx高16位不是0。那么在get_base函数开始处增加一句and $0x0,%%edx,以防止高位溢出便可。
图3.18
编译运行后发现此处仍有错误,再次查看get_base()汇编码如图3.19所示,特别注意标注红色下划线的语句,这些语句拷贝的源地址和目的地址使用了相同的寄存器edx,以edx为基地址取值赋给dh,这时候edx的值一定改变了;接下来又用edx为基地址取值赋给dl时,取到的值肯定不是应该的那个。晕,编译器脑残了吗?
与图3.12对比,发现新编译器对这种老代码的编译并不稳定,之前编译的目的寄存器时ecx,现在却是edx,这中间我做什么了吗?好吧,求指点。
图3.19
将get_base代码由图3.13修改为如图3.20所示,更换目的寄存器为ecx便可区分了。
图3.20
编译运行后,execve系统调用终于通过了。
问题7:
虽然execve成功了,但是加载的shell程序却无法正常运行。具体的错误界面没能留下,十分后悔当初记录不仔细。但shell程序没能正常运行是明确的。这个问题就更加难调试了,因为在执行了exeve之后,首先要进行需求加载,发生79(大约是这个数字)次do_no_page缺页中断从文件系统中加载shell文件,那错误就可能是中断处理时发生的,也可能是shell运行过程中发生的,而shell又是这个系统里面最大的程序,从中寻找一个错误何其困难。
然而幸运的是,这个错误被我偶然间找到了。我在过滤缺页中断处理函数时,每次逻辑块拷贝(共四次)都查看了拷贝结果,发现shell的第一块数据被复制到申请的空闲页的0-0x3ff处,第二个块却被拷贝到0x800-0xbff处,依次类推,隔1024B复制一块数据,这样导致内存中的可执行文件是不连续的。查看COPYBLK函数,其汇编码如图3.21所示。思考一下可知,第一次调用COPYBLK()拷贝数据时,目的地址寄存器edi最后增加了0x400B,这个修改影响了下一次块拷贝。可见还是寄存器保存和恢复的问题啊。
图3.21
增加红色括号标注的语句后COPYBLK代码如图3.22所示。
图3.22
从这个问题的调试过程也说明问题越是复杂时候,细心越是重要。编译运行后结果如图3.23所示,打印出了“===Ok.”这些字符。
图
阅读init函数源码可知,该段功能是用shell执行/etc/rc脚本程序,打开使用hexdump |C rootimage-0.11|less命令打开rc文件,这里需要好好研究下minix文件系统(可参考赵博士的linux内核完全剖析)才能用找到具体的文件。如图3.24所示。可见运行结果正确。
图3.24
其实在这个问题解决后,如果我当时足够冷静,没有其他事情烦扰,应该会首先完成一件事情再继续下去。这件事情就是:为所有汇编写的函数增加寄存器的保存和恢复操作。然而,那几天我太忙了,忙的忘记了思考,于是发生了问题8这个惨剧,是的,对我来说真是个惨剧。
问题8:
上述美丽的输出结果下面却是如图3.25的错误提示信息。革命尚未成功,同志仍需努力啊。错误性质倒是比较明确,就是上述sh程序运行结束后,系统又创建了一个shell进程,用于和用户进行交互,这个进程无法打开某个文件,原因是进程结构体中的pwd元素指向的当前目录inode找不到了,还是有点诡异的。这个shell结束后,再创建下一个,如此循环往复。这个过程就比较熟悉了,就是平时使用linux时的tty里面的命令行嘛。因此凭我超常的男人直觉认为,这是最后一个问题了。
图3.25
然而黎明前的黑暗真黑啊,这个问题也是最难出现错误的程序不在内核源码中,而是从文件系统加载的sh可执行文件,如果分析bash源码可以参考,但是比较费时耗力。因为sh可执行文件是不会出错的,错误肯定发生在被高级编译器编译后的内核程序中,所以可以先偷个懒。经过插桩调试,发现出错的是main.c 中init函数第二个execve系统调用,如图3.26所示。该系统调用加载/bin/sh文件并运行它。使用souce insight的查找引用功能,寻找“No cwd inode”的引用,发现该语句出现在Namei.c的函数中,而get_dir()却是个静态函数,在system.map中不可见,还会被优化编译到sys_mkdir()、sys_open()、Namei()等多函数中。这下彻底迷失了。
图3.26
只好一步一步来了。在系统运行进入第三个do_execve函数后,在Namei()处设置断点,到出错会经过两三个Namei(),跟踪最后一个,单步执行发现get_dir()如图3.27所示的判断没通过,当前进程root和pwd指向的i节点的引用数变为0了。这就相当诡异了,到底是调用什么函数更改了当前进程的root目录或pwd目录的i节点(此刻二者指向同一个inode)呢?
图3.27
为了跟踪出错的位置,必须找到修改根目录inode(通过插桩输出得知,其内存地址为0x1c550,其i_count地址为)的函数,然而通过bochs断点运行发现,sh程序在运行时,发生了79次缺页异常,即触发了79次需求加载功能(为了加快创建进程的速度,改善用户体验,linux不是一下子把可执行文件从文件系统中拷贝到内存中,而是拷贝一页就运行该页的指令,运行完后触发缺页异常,进行下一页的拷贝,这样用户就不会有等待的感觉),通过反复测试得知,在第20页0x1c580中的值由8改为1和第21页由1改为0,从而直接导致Namei无法找到根目录。在第20页,使用bochs的watch write 0x1c580命令,发现系统在执行到0xa85e和0xa964之间的一条指令时修改了该值,根据system.map文件可知该区域是函数。而21页修改该值的是iput(),该函数释放一个inode会将其引用数减1,功能上没有问题。
好了,经过九牛二虎之力把问题找到了,get_empty_inode()就是罪魁祸首。阅读其源码并单步跟踪其执行过程,该函数遍历inode_table(首地址为1c4e0),寻找一个空闲的inode,然后将其memset 0。 如图3.28所示,发现一个运行bug,红色圆圈处原来的语句是break,找到空闲并且没上锁的inode后,本应该跳出while循环,而源码仅仅跳出for循环,之后还得等待解锁,显然是不合理的,应该改为goto语句跳出来。
图3.28
此外,跟踪memset汇编代码如图3.29所示,发现将1c518指向的inode清零后,edi变为,这时memset函数已经结束了,接着get_empty_inode()就利用这个edi为1c518处的空闲inode引用置1,却错误地将1c550出的inode引用置1了,这样正好修改了进程根目录的引用数。
图3.29
修改过程如图3.30所示,其实就是用栈保存和恢复edi寄存器而已。
图3.30
建议将所有的汇编函数都加上push 和pop语句保存和恢复函数中使用的寄存器(比如get_base函数),不然很可能以后还会出现其他错误。我猜想有两个可能的原因:早期gcc与现在gcc的不同,要么就是makefile中去掉的-fcombine-regs编译选项导致没有保存寄存器吧?这里求高人指点。
如图3.31所示,终于可以跑通了,庆贺一下. . . . . .
图3.31
四、总结及展望
花了近一个月的时间,终于把上述工作完成,并记录了下来。这项工作完成的过程中耳边一直萦绕着着老师要论文的催促声,算是不太容易吧。其实总结一下,错误都出现在c和汇编合作的地方,多关注这些地方一定能省好多时间。本文开头已经说过,这个linux-0.11并未完全除尽其BUG,但是用上述方法一直做下去,一定能完成的。不过,我得先去干点别的事情了^_^。