linux内核学习之启动程序模块
linux引导程序解析
bootsect程序,驻留在磁盘的第一个扇区中(0磁道 0磁头 1 扇区)。在BIOS加点检测之后,该引导程序会自动地加载在内存的0x7c00处。
bootsect程序在运行时,会首先将自身移动到0x90000处开始执行,并将从第二个扇区开始的共4个扇区大小的setup程序移动到,紧紧挨着该程序的0x90200处。
然后会使用BIOS中断int13 取当前引导盘的参数,接着在屏幕上显示Loading System的字符串,最后把磁盘上setup后面的system模块加载到内存0x10000开始的地方。随后确定根文件系统的设备号,若没有指定,则根据所保存的引导盘的每磁道扇区数目,判断出盘的类型和种类,并保存在设备号root_dev中。
最后长跳转到setup程序的开始处,执行setup程序。
下面为分析的源代码:
.global begtext , begdata , begbss , endtext , enddata , endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text
SETUPLEN = 4 ! nr of setup - sectors
BOOTSEG = 0x07c0
INITSEG = 0x9000
SETUPSEG = 0x9020
SYSSEG = 0x1000
ENDSEG = SYSSEG + SYSSIZE
ROOT_DEV = 0x306
entry start
start:
! 将bootsect自身移动到0x90000处,并跳转开始执行
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
mov ds , ax
mov es , ax
mov ss , ax
mov sp , # 0xFF00
! 利用BIOS中断INT 13将 setup模块,从磁盘第2个扇区开始读到0x90200开始处,共读4个扇区
load_setup:
mov dx , # 0x0000 ! drive 0 , head 0
mov cx , # 0x0002 ! sector 2 , track 0
mov bx , # 0x0200 ! address = 512 , in INITSEG
mov ax , # 0x0200 + SETUP_LEN ! service 2 , nr of sectors
int 0x13
jnc ok_load_setup
mov dx , # 0x0000 ! 出错则重新执行加载程序
mov ax , # 0x0000
int 0x13
j load_setup
! 利用int13 中断,得到磁盘驱动器的参数,特别是每道磁道的扇区数量
mov ax , 0x0800
mov dl , 0x00
int 0x13
! 重新设置es的值
mov sectors , cx
mov ax , #INITSEG
mov es , ax
! print some message
mov ah , # 0x03 ! 读光标位置,返回光标位置在dx中
xor bh , bh
int 0x10
mov cx , # 24
mov bx , # 0x0007
mov bp , #msg1
mov ax , # 0x1301
int 0x10
! ok we have written the message ,现在开始将system模块加载到0x10000开始处
mov ax , #SYSSEG
mov es , ax
call read_it ! 读磁盘上system模块,es为输入参数
call kill_motor ! 关闭马达
! 确定根文件系统所在的设备号
seg cs
mov ax , root_dev
cmp ax , # 0
jne root_defined
seg cs
mov bx , sectors
mov ax , # 0x0208
cmp bx , # 15
je root_defined
mov ax , # 0x021c
cmp bx , # 18
je root_defined
undef_root:
jmp undef_root
root_defined:
seg cs
mov root_dev , ax
jmpi 0 , SETUPSEG ! 此处跳进setup程序
! 下面是将system模块加载进内存的子函数
sread: .word 1 + SETUPLEN ! sectors read of current track
head: .word 0
track: .word 0
! 保证es在64kb处
read_it :
mov ax , es
test ax , # 0xfff
die: jne die
xor bx , bx
rp_read: ! 接着判断是否已经读入全部的数据,比较当前所读的段是否就是系统数据末端所处的段
mov ax , es ! 如果不是,就跳转至下面的ok1标号处继续读数据
cmp ax , #ENDSEG
jb ok1_read
ret ! 如果到达了系统末端,就结束此循环
ok1_read: ! 计算和验证当前磁道上需要读取的扇区数目,放在ax寄存器中,根据当前磁道还未读取的扇区数和
! 段内数据字节开始偏移的位置,计算如果全部读取这些未读扇区,所读的字节是否会超过64kb的限制
! 若会超过,则根据此次最多能读入的字节数,反算出需要读取的扇区数。
seg cs
mov ax , sectors ! 取每磁道的扇区数
sub ax , sread ! 减去当前磁道已读扇区数
mov cx , ax ! cx = ax 为当前磁道的未读扇区数
shl cx , # 9 ! 当前未读的字节数
add cx , bx ! 此次操作之后,段内偏移地址现在的值
jnc ok2_read ! 若没有超过64kb,则跳转至ok2_read
je ok2_read
! 若加上此次将读取的磁道上所有未读扇区时会超过64kb,则反算出 可以最多加载多少 扇区数目
xor ax , ax
sub ax , bx
shr ax , # 9 ! 转换成扇区数目
ok2_read:
! 读当前磁道上指定开始扇区(cl)和需读扇区数(al)的数据到es:bx开始处。然后将磁道上已经读取的扇区数目
! 与磁道最大扇区数sectors作比较,如果小于sectors说明当前磁道上还有扇区未读
call read_track
mov cx , ax ! cx等于当前操作以读扇区数目
add ax , sread ! 加上当前磁道已读扇区数目
seg cs
cmp ax , sectors ! 如果当前磁道上还有扇区未读,则跳转到ok3_read
jne ok3_read
! 如果该磁道的当前磁头面所有扇区已经读完,则读该磁道的下一磁头面(1号磁头)上的数据,如果已经读完则去读下一磁道
mov ax , # 1
sub ax , head ! 判断当前的磁头号,如果是0磁头,则去读1磁头
jne ok4_read ! 读1号磁头
inc track ! 读下一磁道
ok4_read:
mov head , ax ! 保存当前的磁头号
xor ax , ax ! 清除当前磁道的已读扇区数
ok3_read:
! 如果当前磁道上还有未读的扇区,则首先保存当前磁道的已读扇区数目,然后调整存放数据的开始位置,若小于64kb边界值
! 则跳转到rp_read处,继续读数据
mov sread , ax ! 保存当前磁道的已读扇区数
shl cx , # 9 ! 上次已读扇区数 * 512字节
add bx , cx ! 调整当前段内数据开始位置
jnc rp_read
! 否则说明已经读取64kb数据,此时调整当前段,为读下一段数据作准备
mov ax , es
add ax , # 0x1000
mov es , ax
xor bx , bx
jmp rp_read
! read_track 子程序,读当前磁道上指定开始扇区和需读扇区数的数据到es:bx开始处。
! int 0x13 , ah = 2 ,al = 需读扇区数,es:bx 缓冲区开始位置
read_track:
push ax
push bx
push cx
push dx
mov dx , track
mov cx , sread
inc cx
mov ch , dl
mov dx , head
mov dh , dl
mov dl , # 0
and dx , # 0x0100
mov ah , # 2
int 0x13
jc bad_rt
pop dx
pop cx
pop bx
pop ax
ret
bad_rt: mov ax , # 0
mov dx , # 0
int 0x13
pop dx
pop cx
pop bx
pop ax
jmp read_track
! 关闭软驱马达的子程序
kill_motor:
push dx
mov dx , # 0x3f2
mov al , # 0
outb
pop dx
ret
sectors:
.word 0 ! 存放当前启动软盘每磁道的扇区数
msg1:
. byte 13 , 10
.ascii " Loading system "
. byte 13 , 10 , 13 , 10
.org 508
root_dev:
.word ROOT_DEV ! 这里存放根文件系统的所在设备号
boot_flag:
.word 0xAA55
.text
endtext:
.data
enddata:
.bss
endbss:
2 setup.s程序分析
setup.s是一个操作系统的加载程序,他的主要作用就是利用BIOS的读取机器系统数据,并将这些数据保存到0x90000开始的位置,(覆盖了bootsect程序所在的地方)。这些参数将被内核中相关程序使用。参数诸如光标位置 ,显存等信息。
然后setup程序将system模块从 0x10000-0x8ffff(任务system模块不会超过512kb) ,整体移动到绝对内存地址为0x0000处。
接着加载中断描述表寄存器idtr和全局描述表寄存器gdtr, 开启A20地址线,重新设置两个中断控制芯片8259A,将硬件中断号重新设置为0x20---0x2f。最后设置CPU的控制寄存器CR0,从而进入32位保护模式运行,并跳入到system模块最前面部分的head.s程序继续运行。
为了能让head.s在32位保护模式下运行,在本程序临时设置了中断描述符表(IDT)和全局描述符表(GDT),
在GDT中设置了当前代码段的描述符和数据段的描述符,在head.s中会重新设置这些描述符表。必须使用lgdt把描述符表的基地址告知CPU,再将机器状态字置位即可进入32位保护模式。
SYSSEG = 0x1000
SETUPSEG = 0x9020 ! 本程序所在的段地址
.global begtext , begdata , begbss , endtext , enddata , endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text
entry start
start:
! 保存光标位置已备以后需要
! 这段代码使用BIOS中断取屏幕当前的光标位置,然后保存在内存0x90000处就可以使用
mov ax , #INITSEG
mov ds , ax
mov ah , # 0x03
xor bh , bh
int 0x10 ! 利用BIOS中断 将当前光标位置存档到 dx
mov [ 0 ] , dx ! 将光标位置存放在0x90000处
! 得到内存的大小值
! 利用BIOS中断0x15功能号 ah = 0x88取系统所含扩展内存大小并保存在内存0x90002处。
mov ah , # 0x88
int 0x15
mov [ 2 ] , ax
! 得到显示卡的属性
! 调用BIOS中断0x10,功能号ah = 0x0f
! 返回:ah = 字符列数;al = 显示模式;bh = 当前显示页
! 0x90004存放当前页 ,0x90006存放显示模式,0x90007存放字符列数
mov ah , # 0x0f
int 0x10
mov [ 4 ] , bx ! bh = display page
mov [ 6 ] , ax ! al = video mode , ah = window width
! 检查显示方式并取参数
mov ah , # 0x12
mov bl , # 0x10
int 0x10
mov [ 8 ] , ax
mov [ 10 ] , bx
mov [ 12 ] , cx
! 取得第一个硬盘的信息
mov ax , # 0x0000
mov ds , ax
lds si , [ 4 * 0x41 ]
mov ax , #INITSEG
mov es , ax
mov di , # 0x0080
mov cx , # 0x10
rep
movsb
! 取得第二个硬盘
mov ax , # 0x0000
mov ds , ax
lds si , [ 4 * 0x46 ] ! 取中断向量0x46的值,即hd1的参数值 ------> ds:si
mov ax , #INITSEG
mov es , ax
mov di , # 0x0090 ! 传输目的地址 0x9000 : 0x0090
mov cx , # 0x10
rep
movsb
! 检查系统是否有第二个硬盘
mov ax , # 0x01500
mov dl , # 0x81
int 0x13
jc no_disk1
cmp ah , # 3
je is_disk1
no_disk1: ! 第二块硬盘不存在,所以清空参数表
mov ax , #INITSEG
mov es , ax
mov di , # 0x0090
mov cx , # 0x10
mov ax , # 0x00
rep
stosb
is_disk1:
! 从此开始进入了保护模式
cli
! 首先把system模块移动到正确的位置
mov ax , # 0x0000
cld
do_move:
mov es , ax
add ax , # 0x1000
cmp ax , # 0x9000
jz end_move
mov ds , ax
sub di , di
sub si , si
mov cx , # 0x8000
rep
movsw
jmp do_move
end_move:
! 在此处加载段描述符表,这里需要设置全局描述符表和中断描述符表
mov ax , #SETUPSEG
mov ds , ax
lidt idt_48
lgdt gdt_48
! 打开A20地址线
call empty_8042
mov al , # 0xD1
out # 0x64 , al
call empty_8042
mov al , # 0xDF
out # 0x60 , al
call empty_8042
! 8259芯片主片端口是0x20 - 0x29 ,从片的端口是0xA0 - 0xA9 。
mov al , # 0x11
out # 0x20 , al
.word 0x00eb , 0x00eb
out # 0xA0 , al
.word 0x00eb , 0x00eb
! 8259芯片设置中断号从0x20开始
mov al , # 0x20
out # 0x21 , al
.word 0x00eb , 0x00eb
mov al , # 0x28
out # 0xA1 , al
.word 0x00eb , 0x00eb
mov al , # 0x04
out # 0x21 , al
.word 0x00eb , 0x00eb
mov al , # 0x02
out # 0xA1 , al
.word 0x00eb , 0x00eb
mov al , # 0x01
out # 0x21 , al
.word 0x00eb , 0x00eb
out # 0xA1 , al
.word 0x00eb , 0x00eb
mov al , # 0xFF
out # 0x21 , al
.word 0x00eb , 0x00eb
out # 0xA1 , al
! 下面设置并进入32位保护模式运行,首先加载机器状态字,也称控制寄存器cr0
! 在设置该bit之后,随后的一条指令必须是一条段间跳转指令,一用于刷新当前指令队列
! 因为CPU在执行一条指令之前就已经从内存读取该指令并对其进行解码。
mov ax , # 0x0001
lmsw ax
jmpi 0 , 8
! 下面这个子程序检查键盘命令队列是否为空
empty_8042:
.word 0x00eb , 0x00eb
in al , # 0x64
test al , # 2
jnz empty_8042
ret
! 全局描述符表开始处
gdt:
.word 0 , 0 , 0 , 0
! 代码段选择符的值
.word 0x07FF
.word 0x0000
.word 0x9A00
.word 0x00C0
! 数据段选择符的值
.word 0x07FF
.word 0x0000
.word 0x9200
.word 0x00C0
idt_48:
.word 0
.word 0 , 0
gdt_48:
.word 0x800
.word 512 + gdt , 0x9
.text
endtext:
.data
enddata:
.bss
endbss:
三 head.s程序
功能描述:
head.s程序在被编译生成目标文件之后会与内核其他程序一起被链接成system模块,位于system模块最前面,所以称之为head程序的原因。system模块将被放置在磁盘上setup模块之后开始的扇区中,即从磁盘上第6个扇区开始位置。 linux内核一般大约有120KB ,在磁盘上大概占用240个扇区。
之后我们将在保护模式下编程,head.s使用 as 和ld 编译器和连接器。 这段程序实际上处于绝对地址0处开始的地方,首先是加载各个数据段寄存器,重新设置中断描述符表idt,共256项,并使各个表项指向一个只报错误的哑中断子程序 ignore_int。
在设置了中断描述符表之后,本程序又重新设置了全局段描述符表gdt,主要是把gdt表设置在比较合理的地方,接着设置管理内存的分页处理机制,将页目录表放在绝对物理地址0开始处,紧随后边将放置可以寻址16MB内存的4个页表,并设置它们的表项。
最后,head.s 程序利用返回指令将预先放置在堆栈中的 main.c程序的入口地址弹出,去执行main()程序。
以下是部分代码分析
(1)首先是建立IDT和GDT表
setup_idt:
lea ignore_int , % edx
movl $ 0x00080000 , % eax
movw % dx , % ax
movw $ 0x8E00 , % dx
lea _idt , % edi
mov $ 256 , % ecx
rp_sidt:
movl % eax , ( % edi) #eax的高16位是选择符 ,低16位是段内偏移的低16位
movl % edx , 4 ( % edi) #edx的高16位是段内偏移地址的高16位,低16位是权限位
addl $ 8 , % edi
dec % ecx #重复设置总共256个中断描述符
jne rp_sidt
lidt idt_descr #加载中断描述符表寄存器
ret
setup_gdt:
lgdt gdt_descr #加载全局描述符表寄存器
ret
由代码可知, 256个idt均指向了一个哑中断ignore_int,加载gdt的过程更简单,只是将gdt描述符表的基地址加载进gdtr寄存器。
(2) 页目录表和页表之间的映射
在linux1.1中 , 在绝对内存地址的0x000000处是一个大小为4k的页目录表,然后在内存0x1000,0x2000,0x3000,0x4000处分别是4个页表的首地址,也就是说linux0.11仅仅能访问16M的 内存空间,内存映射的算法如下:
首先在内存0x00000即页目录表设置4个页表首地址,注意添加权限属性。 然后从最后一个页表的最后一个表项,倒序的将物理地址添加进页表中,最后一个页表项的内容是 64M - 4096 + 7 (7表示页面在内存,且用户可读可访问)。
setup_paging: #首先为5页内存进行清空处理
#1个页目录表,4个页表
movl $ 1024 * 5 , % ecx
xorl % eax , % eax
xorl % edi , % edi
cld ; rep ; stosl
#页目录中只需要4个页目录, 7是属性,表示该页存在内存中,且用户可以访问
movl $pg0 + 7 , _pg_dir
movl $pg1 + 7 , _pg_dir + 4
movl $pg2 + 7 , _pg_dir + 8
movl $pg3 + 7 , _pg_dir + 12
#从最后一项 倒序的写入
movl $pg3 + 4092 , % edi #最后一页的最后一项
movl $ 0xfff007 , % eax #16M - 4096 + 7
std
stosl
subl $ 0x1000 , % eax
jge 1b
#设置页目录表基址寄存器cr3的值,指向页目录表。cr3中保存的是页目录表的物理地址
xorl % eax , % eax
movl % eax , % cr3
#设置启动分页处理
movl % cr0 , % eax
orl % 0x80000000 , % eax
movl % eax , % cr0
#该返回指令执行先前压入堆栈的main函数的入口地址
ret
(3)head中还需要为程序跳转进main函数作准备,当完成了页面设置的时候,上面代码的最后一句ret,即
完成了跳入main函数中继续执行。 设置main函数的代码如下:
_tmp_floppy_area:
.fill 1024 , 1 , 0 #共保留1024项,每项1字节,
#下面这些代码为跳转到main函数中,做准备
after_page_tables:
pushl $ 0 #这些是main函数的参数
pushl $ 0
pushl $ 0
pushl $L6 #main函数的返回地址
pushl $_main #_main 是编译程序对main的内部表示法
jmp setup_paging #跳转到建立页表映射
L6:
jmp L6
可以看出执行完setup_paging之后的ret指令,将把_main 加载进 指令寄存器,进行执行。
(4)完整的代码如下
.global _idt , _gdt , _pg_dir , _tmp_floppy_area
_pg_dir: #页目录将会设置在这里,所以该程序会被覆盖掉
startup_32:
movl $ 0x10 , % eax #0x10已经是全局描述符的在描述符表中的偏移值
mov % ax , % ds
mov % ax , % es
mov % ax , % fs
mov % ax , % gs
lss _stack_start , % esp #设置_stack_start -----> ss:esp
call setup_idt #调用设置中断描述符表的子程序
call setup_gdt #调用设置全局描述符表的子程序
movl $ 0x10 , % eax #重新加载所有的段寄存器
mov % ax , % ds
mov % ax , % es
mov % ax , % fs
mov % ax , % gs
lss _stack_start , % esp
#以下代码用来测试A20地址线是否已经打开,采用的方法是向内存0x000000处写入任意的一个数值
#然后看内存地址0x100000是否也是这个数值,如果一样的话,就说明A20地址线没有打开
xorl % eax , % eax
1 : incl % eax
movl % eax , 0x000000 #地址就不需要加$
cmpl % eax , 0x100000
je 1b
movl % cr0 , % eax #
andl $ 0x80000011 , % eax
orl $ 2 , % eax
movl % eax , % cr0
call check_x87
jmp after_page_tables
check_x87:
fninit #向协处理器发送初始化命令
fstsw % ax
cmpb $ 0 , % al
je 1f
movl % cr0 , % eax
xorl $ 6 , % eax
movl % eax , % cr0
ret
.align 2
1 : . byte 0xDB , 0xE4
#建立IDT表
setup_idt:
lea ignore_int , % edx
movl $ 0x00080000 , % eax
movw % dx , % ax
movw $ 0x8E00 , % dx
lea _idt , % edi
mov $ 256 , % ecx
rp_sidt:
movl % eax , ( % edi) #eax的高16位是选择符 ,低16位是段内偏移的低16位
movl % edx , 4 ( % edi) #edx的高16位是段内偏移地址的高16位,低16位是权限位
addl $ 8 , % edi
dec % ecx #重复设置总共256个中断描述符
jne rp_sidt
lidt idt_descr #加载中断描述符表寄存器
ret
setup_gdt:
lgdt gdt_descr #加载全局描述符表寄存器
ret
#这里设置四张页表,可以用来方位16M的内存空间
#每个页表大小为4k,每项为4字节,一张页表可以映射1024 * 4kb的内存空间 ,即4M
.org 0x1000
pg0:
.org 0x2000
pg1:
.org 0x3000
pg2:
.org 0x4000
pg3:
.org 0x5000 #定义下面的内存数据块从偏移0x5000处开始
_tmp_floppy_area:
.fill 1024 , 1 , 0 #共保留1024项,每项1字节,
#下面这些代码为跳转到main函数中,做准备
after_page_tables:
pushl $ 0 #这些是main函数的参数
pushl $ 0
pushl $ 0
pushl $L6 #main函数的返回地址
pushl $_main #_main 是编译程序对main的内部表示法
jmp setup_paging #跳转到建立页表映射
L6:
jmp L6
#下面是默认的中断向量句柄
int_msg:
.asciz " Unknown interrupt\n\r "
.align 2
ignore_int:
pushl % eax
pushl % ecx
pushl % edx
push % ds #入栈占4个字节
push % es
push % fs
movl $ 0x10 , % eax
mov % ax , % ds
mov % ax , % es
mov % ax , % fs
pushl $int_msg #向printk函数传递参数
call _printk #该函数在 / kernel / printk.c中
popl % eax #返回值
pop % fs
pop % es
pop % ds
popl % edx
popl % ecx
popl % eax
iret
.align 2
setup_paging: #首先为5页内存进行清空处理
#1个页目录表,4个页表
movl $ 1024 * 5 , % ecx
xorl % eax , % eax
xorl % edi , % edi
cld ; rep ; stosl
#页目录中只需要4个页目录, 7是属性,表示该页存在内存中,且用户可以访问
movl $pg0 + 7 , _pg_dir
movl $pg1 + 7 , _pg_dir + 4
movl $pg2 + 7 , _pg_dir + 8
movl $pg3 + 7 , _pg_dir + 12
#从最后一项 倒序的写入
movl $pg3 + 4092 , % edi #最后一页的最后一项
movl $ 0xfff007 , % eax #16M - 4096 + 7
std
stosl
subl $ 0x1000 , % eax
jge 1b
#设置页目录表基址寄存器cr3的值,指向页目录表。cr3中保存的是页目录表的物理地址
xorl % eax , % eax
movl % eax , % cr3
#设置启动分页处理
movl % cr0 , % eax
orl % 0x80000000 , % eax
movl % eax , % cr0
#该返回指令执行先前压入堆栈的main函数的入口地址
ret
#140行将压入堆栈的main指令弹出,并跳到main函数中去
.align 2
.word 0
idt_descr:
.word 256 * 8 - 1
. long _idt
.align 2
.word 0
gdt_descr:
.word 256 * 8 - 1
. long _gdt
.align 3
_idt: .fill 256 , 8 , 0 #共256项,每项8字节,初始化为0
_gdt: .quad 0x0000000000000000
.quad 0x00c09a0000000fff
.quad 0x00c0920000000fff
.quad 0x0000000000000000
.fill 252 , 8 , 0
当CPU运行在保护模式下,某一时刻GDT和LDT分别只能有一个,分别有寄存器GDTR和IDTR指定它们的表基址。在某一时刻当前LDT表的基址由LDTR寄存器的内容指定并且使用GDT中的某个描述符来加载,即LDT也是由GDT中的描述符来决定。但是在某一时刻同样也只是由其中的一个被视为活动的。一般对于每个任务使用一个LDT,在运行时,程序可以使用GDT中的描述符以及当前任务的LDT中的描述符