linux内核启动卡死在readl,linux内核学习之启动程序模块

linux引导程序解析

bootsect程序,驻留在磁盘的第一个扇区中(0磁道 0磁头 1 扇区)。在BIOS加点检测之后,该引导程序会自动地加载在内存的0x7c00处。

bootsect程序在运行时,会首先将自身移动到0x90000处开始执行,并将从第二个扇区开始的共4个扇区大小的setup程序移动到,紧紧挨着该程序的0x90200处。

然后会使用BIOS中断int13 取当前引导盘的参数,接着在屏幕上显示Loading System的字符串,最后把磁盘上setup后面的system模块加载到内存0x10000开始的地方。随后确定根文件系统的设备号,若没有指定,则根据所保存的引导盘的每磁道扇区数目,判断出盘的类型和种类,并保存在设备号root_dev中。

最后长跳转到setup程序的开始处,执行setup程序。

下面为分析的源代码:

!SYSSIZE=0x3000.global begtext , begdata , begbss , endtext , enddata , endbss

.text

begtext:

.data

begdata:

.bss

begbss:

.text

SETUPLEN=4!nr of setup-sectors

BOOTSEG=0x07c0INITSEG=0x9000SETUPSEG=0x9020SYSSEG=0x1000ENDSEG=SYSSEG+SYSSIZE

ROOT_DEV=0x306entry start

start:!将bootsect自身移动到0x90000处,并跳转开始执行

mov ax , #BOOTSEG

mov ds , ax

mov ax , #INITSEG

mov es , ax

mov cx , #256sub 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!drive0, head0mov cx , #0x0002!sector2, track0mov bx , #0x0200!address=512,inINITSEG

mov ax , #0x0200+SETUP_LEN!service2, nr of sectorsint0x13jnc ok_load_setup

mov dx , #0x0000!出错则重新执行加载程序

mov ax , #0x0000int0x13j load_setup!利用int13 中断,得到磁盘驱动器的参数,特别是每道磁道的扇区数量

mov ax ,0x0800mov dl ,0x00int0x13!重新设置es的值

mov sectors , cx

mov ax , #INITSEG

mov es , ax!print some message

mov ah , #0x03!读光标位置,返回光标位置在dx中

xor bh , bhint0x10mov cx , #24mov bx , #0x0007mov bp , #msg1

mov ax , #0x1301int0x10!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 , #0jne root_defined

seg cs

mov bx , sectors

mov ax , #0x0208cmp bx , #15je root_defined

mov ax , #0x021ccmp bx , #18je root_defined

undef_root:

jmp undef_root

root_defined:

seg cs

mov root_dev , ax

jmpi0, SETUPSEG!此处跳进setup程序!下面是将system模块加载进内存的子函数

sread: .word1+SETUPLEN!sectors read of current track

head:  .word0track: .word0!保证es在64kb处

read_it :

mov  ax , es

test ax , #0xfffdie: 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 , #1sub 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 , #0x1000mov es , ax

xor bx , bx

jmp rp_read!read_track 子程序,读当前磁道上指定开始扇区和需读扇区数的数据到es:bx开始处。!int0x13, 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 , #0and dx , #0x0100mov ah , #2int0x13jc bad_rt

pop dx

pop cx

pop bx

pop ax

ret

bad_rt: mov ax , #0mov dx , #0int0x13pop dx

pop cx

pop bx

pop ax

jmp read_track!关闭软驱马达的子程序

kill_motor:

push dx

mov  dx , #0x3f2mov  al , #0outb

pop dx

ret

sectors:

.word0!存放当前启动软盘每磁道的扇区数

msg1:

.byte13,10.ascii"Loading system

918e8df969f9f8c8d002f25cda86cade.png".byte13,10,13,10.org508root_dev:

.word ROOT_DEV!这里存放根文件系统的所在设备号

boot_flag:

.word0xAA55.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位保护模式。

INITSEG=0x9000!we move boot here

SYSSEG=0x1000SETUPSEG=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 , #0x03xor bh , bhint0x10!利用BIOS中断 将当前光标位置存档到 dx

mov [0] , dx!将光标位置存放在0x90000处!得到内存的大小值!利用BIOS中断0x15功能号 ah=0x88取系统所含扩展内存大小并保存在内存0x90002处。

mov ah , #0x88int0x15mov [2] , ax!得到显示卡的属性!调用BIOS中断0x10,功能号ah=0x0f!返回:ah=字符列数;al=显示模式;bh=当前显示页!0x90004存放当前页 ,0x90006存放显示模式,0x90007存放字符列数

mov ah , #0x0fint0x10mov [4] , bx!bh=display page

mov [6] , ax!al=video mode , ah=window width!检查显示方式并取参数

mov ah , #0x12mov bl , #0x10int0x10mov [8] , ax

mov [10] , bx

mov [12] , cx!取得第一个硬盘的信息

mov ax , #0x0000mov ds , ax

lds si , [4*0x41]

mov ax , #INITSEG

mov es , ax

mov di , #0x0080mov cx , #0x10rep

movsb!取得第二个硬盘

mov ax , #0x0000mov ds , ax

lds si , [4*0x46]!取中断向量0x46的值,即hd1的参数值------>ds:si

mov ax , #INITSEG

mov es , ax

mov di , #0x0090!传输目的地址0x9000:0x0090mov cx , #0x10rep

movsb!检查系统是否有第二个硬盘

mov ax , #0x01500mov dl , #0x81int0x13jc no_disk1

cmp ah , #3je is_disk1

no_disk1:!第二块硬盘不存在,所以清空参数表

mov ax , #INITSEG

mov es , ax

mov di , #0x0090mov cx , #0x10mov ax , #0x00rep

stosb

is_disk1:!从此开始进入了保护模式

cli!首先把system模块移动到正确的位置

mov ax , #0x0000cld

do_move:

mov es , ax

add ax , #0x1000cmp ax , #0x9000jz end_move

mov ds , ax

sub di , di

sub si , si

mov cx , #0x8000rep

movsw

jmp do_move

end_move:!在此处加载段描述符表,这里需要设置全局描述符表和中断描述符表

mov ax , #SETUPSEG

mov ds , ax

lidt idt_48

lgdt gdt_48!打开A20地址线

call empty_8042

mov  al , #0xD1out#0x64, al

call empty_8042

mov al , #0xDFout#0x60, al

call empty_8042!8259芯片主片端口是0x20-0x29,从片的端口是0xA0-0xA9。

mov al , #0x11out#0x20, al

.word0x00eb,0x00ebout#0xA0, al

.word0x00eb,0x00eb!8259芯片设置中断号从0x20开始

mov al , #0x20out#0x21, al

.word0x00eb,0x00ebmov al , #0x28out#0xA1, al

.word0x00eb,0x00ebmov al , #0x04out#0x21, al

.word0x00eb,0x00ebmov al , #0x02out#0xA1, al

.word0x00eb,0x00ebmov al , #0x01out#0x21, al

.word0x00eb,0x00ebout#0xA1, al

.word0x00eb,0x00ebmov al , #0xFFout#0x21, al

.word0x00eb,0x00ebout#0xA1, al!下面设置并进入32位保护模式运行,首先加载机器状态字,也称控制寄存器cr0!在设置该bit之后,随后的一条指令必须是一条段间跳转指令,一用于刷新当前指令队列!因为CPU在执行一条指令之前就已经从内存读取该指令并对其进行解码。

mov ax , #0x0001lmsw ax

jmpi0,8!下面这个子程序检查键盘命令队列是否为空

empty_8042:

.word0x00eb,0x00ebinal , #0x64test al , #2jnz empty_8042

ret!全局描述符表开始处

gdt:

.word0,0,0,0!代码段选择符的值

.word0x07FF.word0x0000.word0x9A00.word0x00C0!数据段选择符的值

.word0x07FF.word0x0000.word0x9200.word0x00C0idt_48:

.word0.word0,0gdt_48:

.word0x800.word512+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表

#建立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

由代码可知, 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表示页面在内存,且用户可读可访问)。

.align2setup_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+4movl $pg2+7, _pg_dir+8movl $pg3+7, _pg_dir+12#从最后一项 倒序的写入

movl $pg3+4092,%edi #最后一页的最后一项

movl $0xfff007,%eax #16M-4096+7std

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函数的代码如下:

.org0x5000#定义下面的内存数据块从偏移0x5000处开始

_tmp_floppy_area:

.fill1024,1,0#共保留1024项,每项1字节,

#下面这些代码为跳转到main函数中,做准备

after_page_tables:

pushl $0#这些是main函数的参数

pushl $0pushl $0pushl $L6         #main函数的返回地址

pushl $_main      #_main 是编译程序对main的内部表示法

jmp setup_paging  #跳转到建立页表映射

L6:

jmp L6

可以看出执行完setup_paging之后的ret指令,将把_main 加载进 指令寄存器,进行执行。

(4)完整的代码如下

.text

.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 ,%eax1:  incl%eax

movl%eax ,0x000000#地址就不需要加$

cmpl%eax ,0x100000je 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

.align21: .byte0xDB,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

.org0x1000pg0:

.org0x2000pg1:

.org0x3000pg2:

.org0x4000pg3:

.org0x5000#定义下面的内存数据块从偏移0x5000处开始

_tmp_floppy_area:

.fill1024,1,0#共保留1024项,每项1字节,

#下面这些代码为跳转到main函数中,做准备

after_page_tables:

pushl $0#这些是main函数的参数

pushl $0pushl $0pushl $L6         #main函数的返回地址

pushl $_main      #_main 是编译程序对main的内部表示法

jmp setup_paging  #跳转到建立页表映射

L6:

jmp L6

#下面是默认的中断向量句柄

int_msg:

.asciz"Unknown interrupt\n\r".align2ignore_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

.align2setup_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+4movl $pg2+7, _pg_dir+8movl $pg3+7, _pg_dir+12#从最后一项 倒序的写入

movl $pg3+4092,%edi #最后一页的最后一项

movl $0xfff007,%eax #16M-4096+7std

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函数中去

.align2.word0idt_descr:

.word256*8-1.long_idt

.align2.word0gdt_descr:

.word256*8-1.long_gdt

.align3_idt: .fill256,8,0#共256项,每项8字节,初始化为0

_gdt: .quad0x0000000000000000.quad0x00c09a0000000fff.quad0x00c0920000000fff.quad0x0000000000000000.fill252,8,0

当CPU运行在保护模式下,某一时刻GDT和LDT分别只能有一个,分别有寄存器GDTR和IDTR指定它们的表基址。在某一时刻当前LDT表的基址由LDTR寄存器的内容指定并且使用GDT中的某个描述符来加载,即LDT也是由GDT中的描述符来决定。但是在某一时刻同样也只是由其中的一个被视为活动的。一般对于每个任务使用一个LDT,在运行时,程序可以使用GDT中的描述符以及当前任务的LDT中的描述符

posted on 2010-10-08 19:48 kahn 阅读(748) 评论(0)  编辑 收藏 引用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值