linux引导程序解析

2018-11-07 18:58

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

INITSEG = 0x9000 !we move boot here 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表

#建立IDT表

setup_idt:

lea ignore_int , íx movl $0x00080000 , êx movw %dx , %ax movw $0x8E00 , %dx lea _idt , íi

mov $256 , ìx

rp_sidt:

movl êx , (íi) #eax的高16位是选择符 ,低16位是段内偏移的低16位 movl íx , 4(íi) #edx的高16位是段内偏移地址的高16位,低16位是权限位 addl $8 , íi

dec ìx #重复设置总共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表示页面在内存,且用户可读可访问)。

.align 2

setup_paging: #首先为5页内存进行清空处理 #1个页目录表,4个页表 movl $1024 * 5 , ìx xorl êx , êx xorl íi , íi

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 , íi #最后一页的最后一项 movl $0xfff007 , êx #16M - 4096 +7 std stosl

subl $0x1000 , êx jge 1b

#设置页目录表基址寄存器cr3的值,指向页目录表。cr3中保存的是页目录表的物理地址 xorl êx , êx movl êx , %cr3 #设置启动分页处理 movl %cr0 , êx

orl %0x80000000 , êx movl êx , %cr0

#该返回指令执行先前压入堆栈的main函数的入口地址 ret

(3)head中还需要为程序跳转进main函数作准备,当完成了页面设置的时候,上面代码的最后一句ret,即

完成了跳入main函数中继续执行。 设置main函数的代码如下:

.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

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

(4)完整的代码如下

.text

.global _idt , _gdt , _pg_dir , _tmp_floppy_area _pg_dir: #页目录将会设置在这里,所以该程序会被覆盖掉

startup_32:

movl $0x10 , êx #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 , êx #重新加载所有的段寄存器 mov %ax , %ds mov %ax , %es mov %ax , %fs

mov %ax , %gs

lss _stack_start , %esp

#以下代码用来测试A20地址线是否已经打开,采用的方法是向内存0x000000处写入任意的一个数值

#然后看内存地址0x100000是否也是这个数值,如果一样的话,就说明A20地址线没有打开

xorl êx , êx 1: incl êx

movl êx , 0x000000 #地址就不需要加$ cmpl êx , 0x100000 je 1b

movl %cr0 , êx # andl $0x80000011 , êx orl $2 , êx movl êx , %cr0 call check_x87

jmp after_page_tables

check_x87:

fninit #向协处理器发送初始化命令 fstsw %ax cmpb $0 , %al je 1f

movl %cr0 , êx xorl $6 , êx movl êx , %cr0 ret .align 2

1: .byte 0xDB , 0xE4

#建立IDT表

setup_idt:

lea ignore_int , íx movl $0x00080000 , êx movw %dx , %ax movw $0x8E00 , %dx lea _idt , íi mov $256 , ìx

rp_sidt:

movl êx , (íi) #eax的高16位是选择符 ,低16位是段内偏移的低16位 movl íx , 4(íi) #edx的高16位是段内偏移地址的高16位,低16位是权限位 addl $8 , íi

dec ìx #重复设置总共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 \.align 2 ignore_int: pushl êx pushl ìx pushl íx

push %ds #入栈占4个字节 push %es push %fs

movl $0x10 , êx mov %ax , %ds mov %ax , %es mov %ax , %fs

pushl $int_msg #向printk函数传递参数 call _printk #该函数在/kernel/printk.c中

! gdt -- 描述符表的主要作用是将应用程序的逻辑地址转换为线性地址。 gdt:

!///////////////////////////////////////////////////// .word 0,0,0,0 ! dummy,第一个描述符,不可用, ! 主要适用于保护。

!///////////////////////////////////////////////////// !///////////////////////////////////////////////////////

! 系统代码段描述符。加载代码段时,使用这个偏移量。段描述符 ! 一共64位。描述如下 : ! 0 -- 15 limit字段决定段的长度

! 16 -- 39 56 -- 63段的首字节的线性地址 ! 40 -- 43 描述段的类型和存取权限 ! 44 系统标志;如果被清0,则是系统端。

! 45 -- 46 dpl 描述符的特权级;用于限制这个段的存取。它表示 ! 为访问这个段而要求的cpu的最小优先级。 ! 47 -- 1 ! 48 -- 51

! 52 被linux忽略。 ! 53 -- 0 ! 54 -- d/s ! 55 -- g 力度标志 !

.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb) .word 0x0000 ! base address=0 .word 0x9A00 ! code read/exec .word 0x00C0 ! granularity=4096, 386 !

! 从高地址到低地址为 00c0 9a00 0000 07ff ! 那么断脊地址就是00 00 0000 ! 偏移量是 07ff ! 描述符为 07ff !

!/////////////////////////////////////////////////////// !///////////////////////////////////////////////////////

! 系统数据段描述符。当加载数据段寄存器时使用的是这个偏移量。 .word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb) .word 0x0000 ! base address=0 .word 0x9200 ! data read/write .word 0x00C0 ! granularity=4096, 386

!////////////////////////////////////////////////////// idt_48:

.word 0 ! idt limit=0 .word 0,0 ! idt base=0L

gdt_48:

.word 0x800 ! gdt limit=2048, 256 GDT entries .word 512+gdt,0x9 ! gdt base = 0X9xxxx

!///////////////////////////////////////////////////////////////// .text endtext: .data enddata: .bss endbss:


linux引导程序解析.doc 将本文的Word文档下载到电脑 下载失败或者文档不完整,请联系客服人员解决!

下一篇:大力弘扬“三老四严”大庆精神 奋力推进航空发动机行业新跨越汇

相关阅读
本类排行
× 注册会员免费下载(下载后可以自由复制和排版)

马上注册会员

注:下载文档有可能“只有目录或者内容不全”等情况,请下载之前注意辨别,如果您已付费且无法下载或内容有问题,请联系我们协助你处理。
微信: QQ: