*第5章学完了,真像作者说的,“喘口气了”。想到学习第三章时的痛苦,心里开阔了一些:第三章确实是个坎儿,我之前只学了王爽的80X86汇编,没接触过保护模式和80386工作机制,所以第三章劈头盖脸的就是选择子,段描述符,门描述符,gdt,idt,ldt,tss,特权级,堆栈切换等等,彻头彻尾的新知识,而我当时还没意识到(现在想起来这一点才是可怕的),没有心理准备,每天学的都不满意,不开心...不过,虽然方法不对头,但终究一点儿点儿摸过来了,这时才猛发现杨季文那本黑皮书,在这段日子里被翻老了半截。很感谢在网上认识的ganboing,他对我讲学orange's,光有一点8086基础不够,还说到了杨季文的书。对一个自学者而言,书籍有多重要只有他自己知道,这也是在浪费了很多时间与精力后才明白的道理。
2012-10-11
*第五章末尾,orange已经具备了对0~16号异常和基本中断的响应能力,作者说加上bochs自身的调试功能就有了“双保险”。其实不见得,一旦orange自身的idt生效,bochs就不能在异常发生时捕获到异常信息:实际上bochs对异常的处理能力比我们简陋的idt要强的多,他在系统崩溃时候打印出所有寄存器(包括投影寄存器)信息,而且能找到引发程序崩溃的指令并反汇编出来,对于后一点orange的idt就很难做到。所以确切来说并非”双保险“,而是”弱保险“代替了”强保险“:但这没办法,orange总要有自己的idt,我们能做的,就是把idt做的和bochs一样强甚至更强。
*5.54小节,lidt之后,idt立即给了我见面礼:
检查源码,发现是我往8259A送去的ocw是11111110B,时钟中断忘了屏蔽,于是cpu每秒18.2次的在idt里寻找20h中断对应的gate,而我只定义0~16号的gate,cpu寻址偏移显然超出了idt的界限,那就保护异常了。而且是每秒18.2次的保护异常(我在异常处理程序里发送了EOI)。
*一直想用nasm实现这样的一个宏:%macro dbR 1,实现如下效果
dbR ax ;宏替代之后实际是db 'ax'
dbR bx;宏替代之后实际是 db 'bx'
...
我反复尝试了很多次,没做出来。
PS:2012-10-17 解决
原来nasm还有%defstr这个功能,这样用:
--------------------------
%macro dbR 1
%defstr register %1
db register
%endmacro
-------------------------
感谢sholber
2012-10-13
*所谓的“平坦模式”,仍属于保护模式下selector:offset寻址方式的范畴,只不过此时段寄存器的对应描述符的段基址都是0。20位的指针加上4kb的粒度,光靠偏移就达到4G的寻址范围。
c语言用的就是平坦模式(只是不清楚每个c可执行文件是在哪里对ds等段寄存器做的初始化),所以指针的本质是“数据段的偏移值”,因为平坦模式下,由ds指示的数据段基址为0,所以指针的值就对应了线性地址。在汇编里调用c函数时,ds指示的数据段段基址就不一定为0,这时c函数里的c指针很容易寻址失败。看下面一段代码:
test.c
--------------------------------------------
extern void dispInt(int); //dispInt函数用汇编实现,功能是将接受一个int类型参数,并以16进制打印到控制台。
void print_b8000(){
dispInt(*(int *)(0xb8000)); //显然,这里是想把屏幕左上角前两个字符对应的“属性值+ascii码”打印出来
return;
}
-------------------------------------------
接下来在汇编里调用print_b8000函数
---------------------------------------------------------------------------
mdispStrn 'end',ahMod_green ;这里调用我自己实现的一个宏,功能是在控制台打印一个绿色的字符串“end"
call print_b8000
---------------------------------------------------------------------------
编译,链接并在bochs裸机上运行,效果如图
屏幕左上角前两个字符是绿色的“e”,“n”,预期第二行应输出:0x026E 0265,但实际却是“0x2E00 0007”(这两个数完全不着调),显然c指针寻址失败了。因为汇编环境下调用c函数,按照baseOfDS+valueOfPointer计算指针指示的线性地址,这里valueOfPointer是0xb8000没错,但baseOfDS就不一定是平坦模式下的0了。我们试着在调用c函数之前把让ds指向一个段基址为0的数据段,这样指针应该能正常工作了:
---------------------------------------------------------------------------
mov ax,selector_room_plain ;从选择子名字可以猜到,它指示的存储段是平坦模式下的4G内存空间,段基址为0
mov ds,ax
mdispStrn 'end',ahMod_green ;这里调用我自己实现的一个宏,功能是在控制台打印一个绿色的字符串“end"
call print_b8000
---------------------------------------------------------------------------
编译,链接,bochs裸机环境测试:
看第二行,果然是预期的0x026E 0265
做到这一步还是很感慨的:汇编环境下,华丽的指针也走下神坛,露出本来面目。
*c语言里想声明某个函数是在外部用汇编实现的,这样写(以上面的代码为例)
extern void dispInt(int);
别写成extern dispInt,这样写还不如不写,因为不写的话,链接器还能找到dispInt函数(反正实现该函数的汇编文件里声明过“global dispInt”,有这这一句就够了),可一旦写成“extern dispInt”,链接器就会把dispInt当成变量,报错:called object is not a function
*2012-10-30
今晚近距离的体会到:c语言中的变量即是汇编语言中的标签。我在kernel.c中写:
----------------------------------
extern sec_data;//sec_data是我在汇编文件里定义的标签,源码如下:
//sec_data:db 'hello os->world'
//我已经知道sec_data对应的地址是0x31D50
dispInt(sec_data);这是我自己实现的一个函数,原型是dispInt(int);
------------------------------------
结果竟然(现在看来是当然的)输出奇怪的0X6C6C6568,并不是预料中0X31D50.
这就揭露了c语言变量的本质,程序员眼中的a,b,c变量在编译器看来都只是一个个地址数值(例如上面的sec_data对应地址0X31D50),c语言通常不关心一个变量所标识的地址,只关心它所标识地址处的数值——例如上面的0X6C6C6568正是‘hell'的ascii码。
所以上面dispInt(sec_data), c编译器会这样传递参数:push [sec_data],而非push sec_data。这就是c语言对待变量的方式。
我把代码修改了一下:
--------------------------------------
extern sec_data;
dspInt(sec_data);
dispInt(&sec_data);//这里取变量本身的地址
------------------------------------
输出如下图(开头儿的“end”不用管),因为还有其它代码
*2012-11-9
发现用ld链接的话,目标文件的书写次序似乎有讲究,例如我的makefile里:
kernelRelyO= ../lib/kernel.o ../lib/kernel_c.o ../lib/dispStr.o ../lib/proc_asm.o
ld -s -Ttext 0x30400 -o ../bin/kernel.elf $(kernelRelyO)
假如把../lib/kernel.o换到其他位置(只要不是第一个),例如写成:
kernelRelyO= ../lib/kernel_c.o ../lib/dispStr.o ../lib/proc_asm.o ../lib/kernel.o
再make,就发现链接出来的kernel.elf不能执行了(你会发现0X30400地址处的指令根本不是_start标签处的指令)。
我细细想了想,kernel.asm里面存放着入口标签_start,即kernel.o是整个elf文件的入口,是不是因此得把它放在第一位?这似乎牵扯到ld的原理,暂不深究。
*c语言里的NULL常量是定义在stdio.h里面的,gcc不认识null(小写),NULL的字面值其实就是0.