突破BIOS无法连续读取软盘72扇区的限制

具体代码调试和讲解请参看视频:
Linux kernel Hacker, 从零构建自己的内核

一直以来,我们的操作系统加载器,秉承简单够用的原则,只要能把编译好的二进制内核送进内存就可以了,所以加载器的算法是,连续读取软盘扇区,将扇区的内容写入到从0x8000 开始的内存中。以下是我们内核加载器的代码:

org  0x7c00;

LoadAddr EQU  08000h   


entry:
    mov  ax, 0
    mov  ss, ax
    mov  ds, ax
    mov  es, ax
    
    
    mov          BX, LoadAddr       ; ES:BX 数据存储缓冲区
    mov          CH, 1        ;CH 用来存储柱面号
    mov          DH, 0        ;DH 用来存储磁头号
    
readFloppy:
   
    cmp          byte [load_count], 0
    je           beginLoad

    
    mov          CL, 1        ;CL 用来存储扇区号

    mov          AH, 0x02      ;  AH = 02 表示要做的是读盘操作
    mov          AL,  18        ; AL 表示要练习读取几个扇区
    mov          DL, 0         ;驱动器编号,一般我们只有一个软盘驱动器,所以写死   
                               ;为0
    INT          0x13          ;调用BIOS中断实现磁盘读取功能

    inc          CH
    dec          byte [load_count]
    JC           fin
    add          bx, 512 * 18
    jmp          readFloppy
    
beginLoad:
    jmp          LoadAddr


load_count db 3 ;连续读取几个柱面

fin:
    HLT
    jmp  fin

load_count 指的是要读取的软盘柱面数。一个1.44M软盘,其中一个磁面有80个柱面,一个柱面有有两面,上面和背面,每面对应一个磁道,一个磁道有18个扇区,一个扇区有512字节。上面代码中,load_cout 的值设置为3, 也就是程序要连续读取3个柱面,也就是要将软盘中大约 3* 18 * 512 字节,也就是27k的内容写入地址为08000h的内存中。

随着我们开发的操作系统功能越来越强大,其代码量也越来越大,现在内核编译后,已经接近15k了,超过27k是迟早的事情,一旦超过27k,那么我们的软盘在往内存拷贝内核时,就需要连续将4个柱面,也就是72扇区的数据写入到内存中。按照设想,我们只要把上面load_cout的值改成4就可以了。

然而一旦改成4,问题就出现了,因为读取4个柱面,也就要连续向内存读入18*4 = 72个扇区的内容,现在大多数BIOS提供的int 013h软盘读取中断功能,一旦发现调用代码要读取的内容有72扇区以上,它就会返回失败,如果有同学尝试着把上面代码的load_cout该成4,然后再运行程序,就会发现内核加载失败,也就是int 03h 的中断调用返回失败,这样一来,一旦我们的内核大小超过27k, 的话,我们现在的加载器就无法正确加载了。

如果你使用的是虚拟机Bochs 来运行上面的加载器代码,那么连续读取软盘超过72扇区时,Bochs提供的Bios调用会返回失败。我们看看Bochs源码中有个Bios功能的函数如下(rombios.c):

 void
 7273 int13_diskette_function(DS, ES, DI, SI, BP, ELDX, BX, DX, CX, AX, IP, CS, FLAGS)
 7274   Bit16u DS, ES, DI, SI, BP, ELDX, BX, DX, CX, AX, IP, CS, FLAGS;
 7275 {
            ......
            
 7337       if ((drive > 1) || (head > 1) || (sector == 0) ||
 7338           (num_sectors == 0) || (num_sectors > 72)) {
 7339         BX_INFO("int13_diskette: read/write/verify: parameter out of range\n");
 7340         SET_AH(1);
 7341         set_diskette_ret_status(1);
 7342         SET_AL(0); // no sectors read
 7343         SET_CF(); // error occurred
 7344         return;
 7345       }
            ....
    }

这个函数模拟的就是软盘读取BIOS int 013h中断功能,当我们的代码连续读取几个柱面的扇区时,我怀疑Bochs会把这些要读的扇区请求积累起来,然后把要读取的扇区一次性进行写入,而不是请求一次就执行一次读取动作,因此代码中的条件判断num_sectors > 72 就会成立,于是连续读取超过4个柱面也就是72扇区,Bochs模拟器就会返回失败。由于当前很多虚拟机都大量使用Bochs的源代码,或是实现机制类似,我在mac上用的是parallels ,它的反应跟Bochs一样,也是连续读取软盘超过72扇区时,返回了错误,因此在前面的加载器代码中,一旦连续读取4个柱面以上时,读取请求就会返回失败。

为了绕过这个限制,现在我们加载器的做法是,不再一次连续读取18个扇区,而是一次读取一个扇区,把这个扇区的数据先读入一个给定的,大小为512字节的缓冲区内,然后再把该缓冲区的内容,拷贝到指定的内存中,也就是我们要多做一次没有意义的拷贝工作。

由于我们的内核要加载到内存08000h, 因此,我将08000h前512字节,也就是起始地址为07E00h开始的512字节内存作为软盘一个扇区数据的缓冲区,每次从软盘读入一个扇区数据时,先把数据写入到这个缓冲区,然后再把这个缓冲区的数据拷贝到08000h之后的地址,代码如下:

org  0x7c00;

LoadAddr EQU  08000h 
BufferAddr EQU 7E0h

BaseOfStack     equ 07c00h

entry:
    mov  ax, 0
    mov  ss, ax
    mov  ds, ax
    
    mov  ax, BufferAddr
    mov  es, ax
   
    mov  ax, 0
    mov  ss, ax
    mov  sp, BaseOfStack
    mov  di, ax
    mov  si, ax
    

    mov          BX, 0       ; ES:BX 数据存储缓冲区
    mov          CH, 1        ;CH 用来存储柱面号
    mov          DH, 0        ;DH 用来存储磁头号
    mov          CL, 0        ;CL 用来存储扇区号
    
    
;每次都把扇区写入地址 07E00处

readFloppy:
    
    cmp          byte [load_count], 0
    je           beginLoad

    mov          bx, 0
    inc          cl
    mov          AH, 0x02      ;  AH = 02 表示要做的是读盘操作
    mov          AL,  1        ; AL 表示要读取几个扇区
    mov          DL, 0         ;驱动器编号,一般我们只有一个软盘驱动器,所以写死   
                               ;为0
    INT          0x13          ;调用BIOS中断实现磁盘读取功能
    JC           fin


;把刚写入07E00的一个扇区的内容写入从08000h开始的地址

copySector:
    push si
    push di
    push cx

    mov  cx, 0200h  ;缓冲区数据大小,也就是512字节
    mov  di, 0
    mov  si, 0
    mov  ax, word [load_section];es
    mov  ds, ax
   
copy:
    cmp  cx, 0
    je   copyend

    mov  al, byte [es:si] ;es:si指向07E00
    mov  byte [ds:di], al

    inc  di
    inc  si
    dec  cx
    jmp  copy

copyend:
    pop cx
    pop di
    pop si

    mov bx, ds
    add bx, 020h
    mov ax, 0
    mov ds, ax
    mov word [load_section], bx
    mov bx, 0
    
    ;end of copySector

    cmp          cl, 18
    jb           readFloppy

    inc          CH
    mov          cl, 0
    dec          byte [load_count]
    jmp          readFloppy
    
beginLoad:

    mov  ax, 0
    mov  ds, ax
  
    jmp          LoadAddr


load_count db 10 ;连续读取几个柱面
load_section dw 0800h

fin:

    HLT
    jmp  fin

这段代码跟开头的代码,不同之处在于,第一段代码,是连续从软盘读取扇区数据到指定的内存里,这么做可能存在一个隐性的难以发现的问题:

一是编译器可能会对代码进行优化,最终编译出来的二进制代码可能给汇编代码的原意有所不同,编译后的代码可能会把所以读请求积攒起来,然后一次发出读取命令,由于代码要读取4个柱面,每次读取18个扇区,最终编译的代码可能是把4个柱面,总共72个扇区积攒起来,然后一次读取,这样的话,就可能违反了Bochs虚拟机的读取限制。

二是,在读取数据时,只要读取的数据不需要立刻使用的话,CPU可能会将数据读取的请求积累起来,当有数据请求时,才把所有积攒起来的数据读取请求一次发出,从而提高读写效率。

无论是那种情况,都有可能造成一次读取超过72扇区的请求,从而被虚拟机拒绝。改动后的代码是,当读取一个扇区的数据后,程序立马进行数据拷贝,这样的话,上面提到的优化机制就不能产生作用,数据的读取请求就不会被积攒起来,因此就不会遭遇Bochs虚拟机的读取限制。

我们分析下第二段代码,BufferAddr 指的是软盘扇区的数据读取后要写入的缓冲区地址,注意它的值是0x7E0, 为什么是0x7E0,而不是0x7E00呢,这是因为这个值会直接付给段寄存器es, 在实模式下,寻址方式是 段地址:段偏移,转换成直接地址就是 段地址*16 + 段偏移,由于0x7E0 * 16 = 0x7E00, 因此把0x7E00付给段寄存器,就需要把最后一个0去掉,也就是0x7E00 要除以16.

readFloppy 这段代码通过int 013h调用,让BIOS从软盘中读取一个扇区的数据,然后把数据写入到起始地址为0x7E00的缓冲区,接着copySector这段代码把刚写入缓冲区的数据拷贝到内核的加载地址,也就是08000h之后,变量load_section 用来存储的是内核加载的起始地址,由于这个值要付给段寄存器ds,所以它的实际值是0800h而不是08000h,每次往这个地址写入512字节的数据后,下次写入时,地址要往下偏移512字节,512除以16等于32,因此要把load_section的值加上32,也就是020h.

有了上面改动后,加载代码能读取任意多个扇区数据到内存而不用担心Bochs虚拟机的限制,虽然我用的是Virtual Box, 但据说Virtual Box使用的也是Bochs代码,所以当我连续加载扇区超过72时,Virtual Box对代码的数据读取请求也返回识别,使用上面的修改后,数据的读取问题也能得到解决了。

Message Box的计时器效果

最后,我们实现一个计数器效果,在write_vga_desktop.c里,根据下面代码进行更改:

void CMain(void) {
...

for(;;) {
       char* pStr = intToHexStr(counter);
       counter++;
       boxfill8(shtMsgBox->buf, 160, COL8_C6C6C6, 40, 28, 119, 43);
       showString(shtctl, shtMsgBox, 40, 28, COL8_000000,pStr);

       io_cli();
       if (fifo8_status(&keyinfo) + fifo8_status(&mouseinfo)  == 0) {
 
           io_sti();
       }
...
}

我们在主循环里,让一个变量从0开始自加,然后把其结果显示在Message Box的主窗体里,如果大家按照上面代码更改后,会发现界面有明显的闪烁效果。这是我们当前图层的刷新机制导致的,在后面课程中,我将与大家研究如何消除这种令人痛苦的闪烁现象。


这里写图片描述
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容