2012-10-10
*第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.
*2013-5-19
linux下的dd命令往软盘映像写文件,例如:dd if=kernel.elf of=a.img conv=notrunc bs=512,假设kernel.elf文件是512*19字节。那么,软盘的side 0,cylinder 0的18个扇区写满之后,dd会把剩余的512字节写入side 1,cylinder 0,而不是sido 0,cylinder 1。
*2013-5-20
**今天又遇到一个bug,出在这段:
fire_asm: mov esp,[esp+4];point esp to addr_pcb,and reset kernel stack lea eax,[esp+size_stackframe];这句是错的 mov dword [tss+tss_esp0_offset],eax;register pcb.stackfra
代码块摘自fire_asm(int addr)函数的汇编实现,供fire(int pid)函数调用。这几行是根据addr找到pcb.regs的栈底,并把栈底地址注册到tss.esp0
第一句把传递进来的addr,即pcb的地址放到esp;第二句想把栈底地址传送到eax,你看到size_stackframe是c语言模块定义的,有经验的话你一眼就出来用错了。因为size_stackframe拿到汇编里只是一个地址。这样改就好了:
mov esp,[esp+4];point esp to addr_pcb,and reset kernel stack mov ebx,[size_stackframe] lea eax,[esp+ebx] mov dword [tss+tss_esp0_offset],eax;register pcb.stackframe.bottom in tss.esp0
这个bug调了一下午,我是这样一步步找到它的:
进程调度模块新增了cold函数,我在进程p2里调用cold(100),即休眠100个时间片,跟踪调试:cold先使p2挂起,没问题;100个时间片后,p2被唤醒,没问题;唤醒之后,p2重新调度,没问题;但是p2进程体内,cold(100)语句后面的代码没有被执行。
显然,问题出在了“p2被重新调度”这一环节,进程虽然被调度,但是它没有记住上一次挂起的断点,即指向cold函数内__asm__("int $0x20")的下一条指令,可惜它没有。
这就不是cold函数的问题,而是进程调度模块的问题,在此也提醒读者,进程调度模块能在屏幕上打印出“aaa bbbb aaa bbbb”不代表功德圆满,它可能没记住断点,每次都是重新开始运行,并不会引起察觉。(测试进程有没有记住断点,可以这样:启动一个最简单的进程,它只有两句汇编码proc 1:inc eax jmp proc1),不要同时运行其它进程,bochs启动后,不断的[c]运行,[ctrl+c]终止,[r]查看eax寄存器,每次查看时,你会发现eax值比原来增大一些,这就说明进程的断点被保存好了。)
因此,在进程调度代码里检测tss.esp0是否正确,果然发现错了,这样就去找tss.esp0在哪儿被赋值的——在fire_asm里。好了。
*2013-5-21
**gcc默认的char是有符号的,我今天写了个in_byte函数,从60h端口读键盘make code和break code,得到的结果扩展成int类型后总是ffffffffxx,检查in_byte的函数声明,原来返回类型声明成char,这样,返回值大于127时就溢出了。
*2013-5-22
**今天又遇到一个bug。写了第一个系统调用:eax=0(调用功能号),ebx=ascii,ecx=mod,edx=line,esi=column在指定的位置写一个字符。在ring3一调用int0x80,就发生“无效指令异常”,发生异常时,自制的dump_sys函数显示“crack scene>>>ring:0,stack_postion:kernel,ienter:1,path_ring0:0x80...",就是说系统崩溃时,cpu运行在ring0,最后一次进入ring0是通过int 0x80,堆栈是内核态,中断深度为1,无套嵌。
最后检查出来,问题在进程调度模块的fire_asm函数,它在进程调度末尾,负责从ring0切换到ring3
pop gs pop fs pop es pop ds popad add esp,4 mov al,20h out 20h,al iretd
你看到,在popad之后,fire_asm向9258A发送了EOI,mov al,20h....于是eax就被改变了。
系统崩溃的过程是这样的:进程p1执行“mov eax,0”,正准备执行下一条指令"int 0x80",时钟中断上来了,控制权还给p1时,eax就变成0x20,系统还没有20号调用,于是call dword [func_table+eax*4],就跑飞了。
回头看看我的dump_sys函数,反应的信息还是很准的,只是还不够强,它要是能捕获出错指令就好了。
*2013-5-25
**磁盘驱动器原理:
磁盘驱动器是电子计算机中磁盘存储器的一部分,用来驱动磁盘稳速旋转,并控制磁头在盘面磁层上按一定的记录格式和编码方式记录和读取信息。驱 动磁盘转动并在盘面上通过磁头进行写入读出动作的装置。磁盘装在驱动器上,以恒速旋转。磁头浮动在盘片表面。在磁盘控制器的控制下,经磁头的电磁转换在盘 面磁层上进行读写数据操作。硬磁盘驱动器分头臂固定型和头臂移动型两类。头臂移动型硬磁盘驱动器又可分为可互换式与固定式两类。
**oranges'第9章上来的磁盘端口操作看上去好麻烦,在知道上遇到一篇IDE磁盘端口的回答,很清楚:http://zhidao.baidu.com/question/11113094.html
1:INSW,串输入指令,以字单位,该指令的功能是从DX指定的端口读入一个 字节到ES:DI指定的内存单元中 2:ide硬盘就是用80针的排线,或者40针的排线。简单的说就是普遍使用的那 种宽口的。接口处上下两排眼,下排中间有一个眼是堵死的。 相对的,硬盘上也是两排针,下排中间少一根 硬盘读写端口的具体含义 对硬盘进行操作的常用端口是1f0h~1f7h号端口,各端口含义如下: 端口号 读还是写 具体含义 1F0H 读/写 用来传送读/写的数据(其内容是正在传输的一个字节的数据) 1F1H 读 用来读取错误码 1F2H 读/写 用来放入要读写的扇区数量 1F3H 读/写 用来放入要读写的扇区号码 1F4H 读/写 用来存放读写柱面的低8位字节 1F5H 读/写 用来存放读写柱面的高2位字节(其高6位恒为0) 1F6H 读/写 用来存放要读/写的磁盘号及磁头号 第7位 恒为1 第6位 恒为0 第5位 恒为1 第4位 为0代表第一块硬盘、为1代表第二块硬盘 第3~0位 用来存放要读/写的磁头号 1f7H 读 用来存放读操作后的状态 第7位 控制器忙碌 第6位 磁盘驱动器准备好了 第5位 写入错误 第4位 搜索完成 第3位 为1时扇区缓冲区没有准备好 第2位 是否正确读取磁盘数据 第1位 磁盘每转一周将此位设为1, 第0位 之前的命令因发生错误而结束 写 该位端口为命令端口,用来发出指定命令 为50h 格式化磁道 为20h 尝试读取扇区 为21h 无须验证扇区是否准备好而直接读扇区 为22h 尝试读取长扇区(用于早期的硬盘,每扇可能不是512字节,而是128字节到1024之间的值) 为23h 无须验证扇区是否准备好而直接读长扇区 为30h 尝试写扇区 为31h 无须验证扇区是否准备好而直接写扇区 为32h 尝试写长扇区 为33h 无须验证扇区是否准备好而直接写长扇区 注:当然看完这个表你会发现,这种读写端口的方法其实是基于磁头、柱面、扇区的硬盘读写方法,不过大于8G的硬盘的读写方法也是通过端口1F0H~1F7H来实现的^_^
*2013-5-26
**第9章 才注意到oranges'的port_read函数:
void port_read(u16 port, void* buf, int n); ; ======================================================================== port_read: mov edx, [esp + 4] ; port mov edi, [esp + 4 + 4] ; buf mov ecx, [esp + 4 + 4 + 4] ; n shr ecx, 1 ;但是 cld rep insw ;虽然 ret ; ========================================================================
注意了,它的n是表示从起始端口load到buf的n个byte,而非n个word。你看,虽然汇编码用的是insw,但前面有一句shr ecx,1。
**遇到一份讲IDE端口特别清楚的论文,徐小玲 《IDE接口读写技术》
**这篇提到了更多的IDE端口的零碎知识:http://www.cnblogs.com/weiweishuo/archive/2013/05/26/3100254.html\
13 june
**做页映射的时候,写了这样一个函数:
void map_pg(u32*dir,int vpg_id,int ppg_id,int us,int rw){
// CHECK_DIRENT(dir,PG_H10(vpg_id));
check_dirent(dir,PG_H10(vpg_id));
u32*tbl=(u32*)(dir[PG_H10(vpg_id)]>>12<<12);
tbl[PG_L10(vpg_id)]=ppg_id<<12|us|rw|PG_P;
}
注意到代码块里第一行的宏被注释掉了没?
这里不能用宏,它会莫名奇妙的修改pgbmp区,用函数重写这个宏问题就消失了。是消失了,不是解决了,因为我弄不明白问题原先出在哪儿。标记之。
宏 #define CHECK_DIRENT(dir,entry_id) dir[entry_id]=((dir[entry_id]&PG_P)==0?(PG_RWW|PG_P|PG_USU|alloc_page()<<12):dir[entry_id])
函数 void check_dirent(u32*dir,int entry_id){ if((dir[entry_id]&PG_P)==0){ int ppg_id=alloc_page(); dir[entry_id]=PG_RWW|PG_USU|PG_P|ppg_id<<12; oprintf("meet empty dir-entry,alloc a physical page for linking table,ppg_id=%x\n",ppg_id); } }