开篇的废话 当处理器的RESET 引脚的电平由低变高时,处理器会执行一个硬件初始化,以及一个可选的内部自测试(BIST),然后将内部寄存器初始化到一个预置的状态。
在刚刚启动时,CPU从一个特定的地址开始执行指令。
以8086为例,这个地址是 0xFFFF0
(由初始化的CS:IP 决定,此时的 CS 和 IP 分别是 0xF000和0xFFF0)。
16 位 CPU ,8086 寻址空间为 1M ,0xF FFF0
是实际上是内存空间中的最后一条指令。
以80386为例,这个地址是0xFFFFFFF0
(此时的CS和IP 分别是0xF000和0xFFF0)。
注意,32位CPU 下,这里采用的地址计算方式不是CS * 0x10+IP ,而是使用 段描述符高速缓存器中的段基地址。
如果使用bochs
调试就可以看到此时的 CS 描述符高速缓存器中的段基地址为 0xffff0000
,此时的 IP 为0xFFF0
,所以最终指向的第一条指令为0xFFFF FFF0
,同样,这个地址是内存空间中的最后一条指令。
在32位CPU 下,该指令是jmpf 0xf000:e05b
,跳转执行的内存地址为 0xfe05b
(在1M 内存内执行),实际上该地址就是ROM-BIOS 部分。
ROM-BIOS 会被映射到 1M 内存和4G的最后部分(具体地址请自行考究)
在执行了 BIOS 之后,BIOS 会将存储设备(软盘或硬盘)的头512字节数据加载到0x7c00,并且会检查512字节的内容是否以 0x55
和0xaa
结尾,否则引导程序无效。
关于这个地址为什么是 0x7c00
,你可以参考 为什么主引导记录的内存地址是0x7C00?
Image 是 Linux 0.12 的镜像文件
在 Linux 0.12 中,这个 头 512 字节实际上就是 bootsect.S
。
bootsect.S
源码1 2 as86 -0 -a -o bootsect.o bootsect.s ld86 -0 -s -o bootsect bootsect.o
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 BOOTSEG = 0x07c0 INITSEG = 0x9000 entry start start: mov ax,#BOOTSEG mov ds,ax mov ax,#INITSEG mov es,ax mov cx,#256 sub si,si sub di,di rep movw jmpi go,INITSEG go: mov ax,cs
将0x7c00
开始的512字节(其实就是引导扇区bootsect
部分)复制到0x90000
开始的内存位置,最后跳转到0x9000:go
位置执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 go: mov ax,cs mov dx,#0xfef4 ! arbitrary value >>512 - disk parm size mov ds,ax mov es,ax push ax mov ss,ax ! put stack at 0x9ff00 - 12. mov sp,dx #设置 ds 、es ss 段寄存器的的值为0x9000 #为什么设置栈指针为0xfef4,实际栈指针大于等于512即可 #还需考虑到后续还要加载 setup 的大小 #在linux 0.11 中,这个值是0x9ff00,这里为了放软盘参数表,准备了12字节的空间留待后续使用 #0x9000:0xfef4 = 0x9 FEF4 + 12 = 0x9 ff00 push #0 pop fs mov bx,#0x78 ! fs:bx is parameter table address seg fs lgs si,(bx) ! gs:si is source mov di,dx ! es:di is destination mov cx,#6 ! copy 12 bytes cld rep seg gs movw mov di,dx movb 4(di),*18 ! patch sector count #从fs:bx处读出软盘参数表的地址,低字软盘参数表的偏移地址读到SI 中,高字软盘参数表的段地址读到GS中 #将软盘参数表复制到0x9000:0xfef4 ,即上面的空出来的地址 #修改软盘扇区中的偏移为4的位置,设置为18 #该参数是软盘参数中每磁道的最大扇区数 #注意:0x0 0078 处并不是一个中断向量 seg fs mov (bx),di seg fs mov 2(bx),es #修改软盘参数表中的偏移地址和段地址,使其指向修改后的软盘参数表位置 0x9000:0xfef4 pop ax mov fs,ax mov gs,ax #fs gs = 0x9000 xor ah,ah ! reset FDC xor dl,dl int 0x13 #调用0x13 中断,重置 第一块软盘 的磁盘系统
读取setup
模块 接着准备读取setup
模块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 SETUPLEN = 4 ...... load_setup: xor dx, dx ! drive 0, head 0 mov cx,#0x0002 ! sector 2, track 0 mov bx,#0x0200 ! address = 512, in INITSEG mov ax,#0x0200+SETUPLEN ! service 2, nr of sectors int 0x13 ! read it jnc ok_load_setup ! ok - continue ;jnc - jump not cf push ax ! dump error code call print_nl mov bp, sp call print_hex pop ax xor dl, dl ! reset FDC xor ah, ah int 0x13 j load_setup ; j - jmp #调用 0x13 号中断,从 0x9000:0x200 开始读取4个扇区 #磁头为0,驱动器为 0 的软盘 #柱面为0,扇区号为2 #如果返回码(CF = 0 )表示读取成功 #否则打印错误的状态码,重置 第一个软盘的磁盘系统,重新读取扇区 ok_load_setup: ! Get disk drive parameters, specifically nr of sectors/track 。 xor dl,dl mov ah,#0x08 ! AH=8 is get drive parameters int 0x13 xor ch,ch seg cs mov sectors,cx 。 mov ax,#INITSEG mov es,ax #使用 0x13 中断的 08功能读取驱动器的参数,ES:DI = 0x900:0xfef4 #重新读取软盘参数表 #CX 中是柱面数和扇区数 #保存到标号 sectors ! Print some inane message mov ah,#0x03 ! read cursor pos xor bh,bh int 0x10 mov cx,#9 mov bx,#0x0007 ! page 0, attribute 7 (normal) mov bp,#msg1 mov ax,#0x1301 ! write string, move cursor int 0x10 #使用 0x10 中断的 3 号功能,读取光标的位置 #使用 0x10 中断的 0x13 号功能,往显示区域写入字符 ! ok, we've written the message, now ..... sectors: .word 0 #初始化内容是2个字节的0x00 msg1: .byte 13,10 #13 10 是 CR LR 的ASCII 码 .ascii "Loading"
读取system
模块 将system
模块读取到内存中:
1 2 3 4 5 6 7 8 mov ax,#SYSSEG mov es,ax ! segment of 0x010000 call read_it ; 读磁盘上system模块 call kill_motor ; 关闭驱动器马达 call print_nl #SYSSEG = 0x1000 #调用了3个例程,其中最后复杂的是 read_it #读取 system 到 0x1000:0x00 处
依次来看,首先看read_it
例程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 read_it: mov ax,es test ax,#0x0fff die: jne die ! es must be at 64kB boundary xor bx,bx ! bx is starting address within segment rp_read: mov ax,es cmp ax,#ENDSEG ! have we loaded all yet? ; 是否已经加载了全部数据? jb ok1_read ret #使用 teset 指令测试 SYSSEG 的值的低12位是否为0 #低12位为0 ,意味着所对应的地址在64 KB 的边界处(test指令可能导致其他问题,列入0x00 不会导致死循环) #如果 SYSSEG 低于 ENDSEG 的值就 jmp 至 ok1_read #否则 说明已经高于 ENDSEG,返回调用者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ok1_read: seg cs mov ax,sectors sub ax,sread mov cx,ax shl cx,#9 add cx,bx jnc ok2_read je ok2_read xor ax,ax sub ax,bx shr ax,#9 #从标号sectors 读取每磁道的扇区数 #减去当前磁道已读扇区数,得到当前磁道未读扇区数 #并且计算当前磁道扇区数占用的字节数(*512)+偏移(初始值为0)是否超过了64KB #没有超过则jmp 至 ok2_read #如果超过,则计算最多能读取的字节数 #0 - 偏移 = 最多能读取的字节数 #该值右移9位(/512)即为最多能读取的扇区数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 ok2_read: call read_track ; 读当前磁道上指定扇区和需读扇区数的数据 mov cx,ax add ax,sread seg cs cmp ax,sectors jne ok3_read mov ax,#1 sub ax,head jne ok4_read inc track ok4_read: mov head,ax xor ax,ax ok3_read: mov sread,ax shl cx,#9 add bx,cx jnc rp_read mov ax,es add ah,#0x10 mov es,ax xor bx,bx jmp rp_read #ok2_read 例程 #read_track 的操作成功后的返回参数为 AH=00 AL=传输的扇区数 #标号 sread 处存放的是当前磁道已读扇区数 #传输的扇区数 + 当前磁道已读扇区数 =?当前磁道扇区数(标号sectors) #如果小于则说明当前磁道未读,jmp 至 ok3_read。 #如果不小于,说明当前磁道所有扇区已经读取,应该读取下一个磁道的的 1号磁头 #如果是0号磁头,则 jmp 至 ok4_read #否则读取下一个磁道 #ok4_read 例程 #将当前磁道号回填 标号head 处 #ok3_read 例程 #将 标号 head 当前已读扇区数置0(为什么?因为换了磁头) #检查上次读的扇区数量加偏移是否超出了 64 KB #如果没有超出则 jmp 至 rp_read 继续读取 #如果超出则调整 段地址为下一个64KB 开始处,偏移置0 ,jmp 至 rp_read
这里反复提到rp_read
例程,该例程其实很简单:
1 2 3 4 5 6 7 8 9 10 11 12 13 rp_read: mov ax,es cmp ax,#ENDSEG ! have we loaded all yet? jb ok1_read ret #检测当前的段地址是否小于 ENDSEG #如果小于,说明system还没读完,jmp 至 ok1_read 继续读取 #如果不小于,说明system已经读取完成,返回。 #题外话 #这里 system 段地址起始地址为 0x1000 结束地址为 0x4000 #也就是说,system 的大小为 起始内存为 0x10000 结束内存为 0x40000 #即 192 KB 的空间,后面编译的时候验证
最后查看read_track
例程,这也是真正读取扇区到内存中的部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 read_track: pusha ; push all pusha mov ax, #0xe2e ! loading... message 2e = . mov bx, #7 int 0x10 popa mov dx,track mov cx,sread inc cx mov ch,dl mov dx,head mov dh,dl and dx,#0x0100 mov ah,#2 push dx ! save for error dump push cx push bx push ax int 0x13 jc bad_rt add sp, #8 popa ret #首先调用 0x10 中断打印一串提示信息 #从标号 track 获取当前磁道号 #从标号 sread 获取已读扇区号 + 1 作为起始扇区 #从标号 head 获取当前磁头号 #调用 0x13 中断读取扇区 #注意 AL 扇区数来自 ok1_read 例程的计算 bad_rt: push ax ! save error code call print_all ! ah = error, al = read xor ah,ah xor dl,dl int 0x13 add sp, #10 popa jmp read_track #bad_rt 例程作为处理读取错误的情况存在 #调用 print_all 打印之前保存在栈中的相关寄存器 #并且 重置 软盘的磁盘操作系统,从栈中恢复在读取扇区之前的寄存器参数,调用 read_track 重新读取扇区
这段代码真的读的我难受。
在磁头号、磁道号、扇区号的处理中参数太多,很容易理不清。
以上代码请配合 13h
和10h
中断的功能表进行参考,相关参数并未做说明。
读取system
模块到内存之后,调用kill_motor
例程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 kill_motor: push dx mov dx,#0x3f2 xor al, al outb pop dx ret #0x3f2 是软盘控制器的只写端口号 #AL =0x00 #软盘驱动器A #复位FDC #禁止DMA和中断请求 #关闭软驱A的马达
最后print_nl
例程进行回车换行。
参考:
软盘控制器的编程方法
检查根文件系统设备 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 ROOT_DEV = 0 SWAP_DEV = 0 seg cs mov ax,root_dev or ax,ax jne root_defined seg cs ; 取出sectors的值(每磁道扇区数) mov bx,sectors mov ax,#0x0208 ! /dev/PS0 - 1.2Mb cmp bx,#15 ; sectors=15则说明是1.2MB的软驱 je root_defined mov ax,#0x021c ! /dev/PS0 - 1.44Mb cmp bx,#18 ; sectors=18则说明是1.44MB的软驱 je root_defined undef_root: jmp undef_root root_defined: seg cs mov root_dev,ax jmpi 0,SETUPSEG #如果 root_dev 为不为0 则 jmp 至 root_defined #如果 root_dev 为0 则取出 标号 sectors (每磁道的扇区数量)处的值 #如果该值为 15 则设置逻辑设备号为 0x0208 #如果该值为 18 则设置逻辑设备号为 0x021c #最后将AX(即逻辑设备号)的值回填到标号 root_dev 处 #最后jmp 至 SETUPSEG:0x0000 #SETUPSEG 值为0x9020 ,即 jmp 0x90200 #也就是setup 加载的位置 #如果没有更改的话,这个的值为 默认 0x301 代表第一个硬盘的第一个分区
这里的 ROOT_DEV
由tools
下的build 程序 写入,来看看具体是如何写入的。
看了 Makefile 执行的命令如下:
看了下源码,C 还不太熟,调试了了下:
argc
=4 也就是说:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #define DEFAULT_MAJOR_ROOT 3 #define DEFAULT_MINOR_ROOT 1 if (argc > 4 ) { if (strcmp (argv[4 ], "FLOPPY" )) { if (stat(argv[4 ], &sb)) { perror(argv[4 ]); die("Couldn't stat root device." ); } major_root = MAJOR(sb.st_rdev); minor_root = MINOR(sb.st_rdev); } else { major_root = 0 ; minor_root = 0 ; } } else { major_root = DEFAULT_MAJOR_ROOT; minor_root = DEFAULT_MINOR_ROOT; } .... buf[508 ] = (char ) minor_root; buf[509 ] = (char ) major_root;
1 2 objdump -m i8086 -b binary -D Image --stop-address 0x200 xxd -g 1 -seek 508 -l 2 Image
而偏移 508 和 509 处在 bootsect
中是预留好的:
1 2 3 4 5 6 7 8 .org 506 swap_dev: .word SWAP_DEV root_dev: .word ROOT_DEV boot_flag: .word 0xAA55a
小结 512 字节bootsect
被安排的非常清楚,没有一个字节是多余的。
这段程序我一开始我以为我会读的非常容易。
直到读到了 “读取 system 模块”部分,这部分参数太多,看着脑子乱,发现看不进去,看的恶心。
进而这几天都在摸鱼想这个(说明好的代码可读性的重要性)。
今天为什么能读下来了?我觉得原因有几个:
即使已经读完这部分代码,我仍然对扇区部分有点迷糊,我找到一篇比较好的博客,有兴趣的可以参考该部分 。
参考
x86汇编语言:从实模式到保护模式——p33