操作系统|内核引导程序---head

1.简介 head.s 程序在被编译生成目标文件后会与内核其他程序一起被链接成 system 模块,它位于 system 模块的最开始部分。system模块将被放置在磁盘上setup模块之后的扇区,从磁盘上第6个扇区开始放置。
注:这段程序处于绝对地址0x00000处。
程序进入保护模式,程序采用AT&T语法格式。
Linux AT&T汇编语法简介:
添加链接描述
作用:head.s程序:设置中断描述符表项(哑中断);检查A20;测试是否有协处理器;初始化内存页目录表;跳转到main.c执行内核初始化
head完成后完成了内存页目录和页表的设置,并重新设置了内核实际使用的中断描述符表idt和全局描述符表GDT,还为软盘驱动程序开辟了1KB字节的缓冲区。
32位下寻址
将实模式下的段寄存器当作保护模式下的段描述符的指针使用,此时段寄存器中存放的是一个描述符在描述符表中的偏移地址寄存器,而当前描述符表的基地址则保存在描述符表寄存器中。
head程序结束后内存映像
操作系统|内核引导程序---head
文章图片

.align

.align2 完整格式 :.align val1,val2,val3 val1 是需要对齐的值 val2 填充字节指定的值 val3 指明最大用于填充或跳过的直接数

.align是汇编语言指示符。其含义是边界对齐调整。”2”表示把随后的代码或数据的偏移位置调整到地址值最后 2 比特位为零的位置,即按 4(=2^2)字节方式对齐内存地址。不过现在 GNU as 直接写出对齐的值而非 2 的幂次。使用该指示符的目的是为了提高 32 位 CPU 访问内存中代码或数据的效率。
.ORG
ORG伪指令用来表示起始的偏移地址,紧接着ORG的数值就是偏移地址的起始值。ORG伪操作常用来指定数据的存储地址,有时也用来指定代码段的起始地址
fill
fill伪指令的格式是 .fill repeat,size,value
表示产生 repeat 个大小为 size 字节的重复拷贝。size 最大是 8,size 字节的值是 value.
按位异或 xor
1. 使某些特定的位翻转 例如要使 EAX 的 b1 位和 b2 位翻转: EAX = EAX ^ 000001102.不使用临时变量就可以实现两个值的交换 例如 a=11110000,b=00001111,要交换a、b的值 a = a^b;//a=11111111 b = b^a;//b=11110000 a = a^b;//a=000011113.在汇编语言中经常用于将变量置零 xor eax,eax4.快速判断两个值是否相等 ?例如判断两个整数a、b是否相等,可通过下列语句实现: ?return ((a ^ b) == 0);

LSS指令
格式:LSS r32,m16:32#用内存中的长指针加载 SS:r32 m16:32表示一个内存操作数,这个操作数是一个长指针,由2部分组成:16位的段选择 子和32位的偏移。

2.源码分析 1.启动32位程序
startup_32: movl $0x10,%eax; 0x10 GDT中的偏移值(一个描述符表项的选择符) ; 请求特级权0(位0-1 =0),GDT(位2=0),选择表中第2项(位3-15=2) ; 正好指向数据段描述符项 mov %ax,%ds; 设置ds,es,fs,gs为setup中构造的数据段的选择符 =0x10 mov %ax,%es; 并将堆栈放置在stack_start(指针)指向的user_stack数组区 mov %ax,%fs mov %ax,%gs lss _stack_start,%esp; _stack_start-->ss:esp,设置系统堆栈 ; 移动到任务0执行(init/main.c),该栈就被用做任务0和任务1共同使用的用户栈 call setup_idt; 调用设置中断描述符表子程序 call setup_gdt; 调用设置全局描述符表子程序 movl $0x10,%eax# reload all the segment registers mov %ax,%ds# after changing gdt. CS was already mov %ax,%es# reloaded in 'setup_gdt' mov %ax,%fs# 因为修改了gdt,所以重新加载这些寄存器 mov %ax,%gs

2.加载各个段寄存器,重新设置中断描述符表,共256项,并使各个表项均指向一个只报错误的哑中断程序,重新设置全局描述符表,
1)IDT:
; 设置中断描述符表(IDT)字程序setup_idt ; 将中断描述符表设置具有256个项,并都指向ignore_int中断门,然后加载中断描述符表寄存器 ; 中断描述符表中的项为8个字节,称为门描述符 setup_idt: lea ignore_int,%edx# 将ignore_int有效地址值赋值给edx movl $0x00080000,%eax # 将选择符0x0008赋值给eax的高16位 movw %dx,%ax/* selector = 0x0008 = cs */ movw $0x8E00,%dx /* interrupt gate - dpl=0, present */ lea _idt,%edi# _idt 是中断描述符表的地址 mov $256,%ecx rp_sidt: movl %eax,(%edi)# eax -> [edi] 将哑中断门描述符存入表中 movl %edx,4(%edi)# edx -> [edi+4] addl $8,%edi# edi + 8 -> edi dec %ecx jne rp_sidt lidt idt_descr#加载中断描述符表寄存器 ret

2)GDT
# 设置全局描述符表项 setup_gdt: lgdt gdt_descr# 加载全局描述符寄存器 ret

3.对比物理地址0与1M开始处的内容是否相同,如果相同那么没有开启A20地址线,进入死循环
; 测试A20地址线是否开启 ; 方法:向内存地址0处写入任意一个数值,看内存地址是否是这个数值 ; 如果是就一直比较下去,产生死循环, ; 表示A20线没有选通,内核就不能使用1MB以后的内存空间 xorl %eax,%eax 1: incl %eax# check that A20 really IS enabled # 1-->标号,表示活动位置计数的当前值,并可以作为指令的操作数 movl %eax,0x000000 # loop forever if it isn't cmpl %eax,0x100000 je 1b# Nb:引用先前最近的标号 'b':backwards 向后 # Nf:引用下一个标号'f':forwards向前 # 这里‘1b’表示:向后跳转到标号1去 # 如果是5f,则是向前跳转到标号5去

4.测试PC机是否含有数据协处理器芯片,并在控制寄存器CR0中设置相应的标志位
; 检测486数学协处理器芯片是否存在 ; 方法:修改控制寄存器CR0,然后执行一条协处理器指令,若出错,则不存咋 ; 需要设置协处理器仿真位(EM)位2,并复位协处理器存在标志(MP)位2 movl %cr0,%eax# check math chip andl $0x80000011,%eax # Save PG,PE,ET /* "orl $0x10020,%eax" here for 486 might be good */ orl $2,%eax# set MP并复位协处理器存在标志(MP) movl %eax,%cr0 call check_x87 jmp after_page_tables

5.设置管理内存的分页处理机制,将页目录表放在绝对物理地址0开始处紧随其后放置共可寻址16MB内存的4个页表,并分别设置它们的表项
【操作系统|内核引导程序---head】6.最后利用返回指令将预先放置在堆栈中的/init/main.c程序的入口地址弹出,运行main()程序
# 下面几个入栈操作为了跳转到init/main.c中main()函数做准备 # 前面三个入栈0值分别表示envp,argv指针和argc的值 after_page_tables: pushl $0# These are the parameters to main :-)main函数参数 envp pushl $0# argv指针 pushl $0# argc pushl $L6# return address for main, if it decides to. 模拟调用main时首先将返回地址入栈的操作 pushl $_main# _main--->main jmp setup_paging# main函数到不了这里 ,到标号L6这里,是一个死循环 L6: jmp L6# main should never return here, but # just in case, we know what happens.

3.head完整源码
/* *linux/boot/head.s * *(C) 1991Linus Torvalds *//* *head.s contains the 32-bit startup code. * * NOTE!!! Startup happens at absolute address 0x00000000, which is also where * the page directory will exist. The startup code will be overwritten by * the page directory. */ ; head程序含有32启动程序代码 ; 32启动程序代码是从绝对地址0x00000处开始 ; 页目录也在该内存,以后启动代码将会被覆盖 .text .globl _idt,_gdt,_pg_dir,_tmp_floppy_area _pg_dir:; 页目录将会放在在这里 startup_32: movl $0x10,%eax; 0x10 GDT中的偏移值(一个描述符表项的选择符) ; 请求特级权0(位0-1 =0),GDT(位2=0),选择表中第2项(位3-15=2) ; 正好指向数据段描述符项 mov %ax,%ds; 设置ds,es,fs,gs为setup中构造的数据段的选择符 =0x10 mov %ax,%es; 并将堆栈放置在stack_start(指针)指向的user_stack数组区 mov %ax,%fs mov %ax,%gs lss _stack_start,%esp; _stack_start-->ss:esp,设置系统堆栈 ; 移动到任务0执行(init/main.c),该栈就被用做任务0和任务1共同使用的用户栈 call setup_idt; 调用设置中断描述符表子程序 call setup_gdt; 调用设置全局描述符表子程序 movl $0x10,%eax# reload all the segment registers mov %ax,%ds# after changing gdt. CS was already mov %ax,%es# reloaded in 'setup_gdt' mov %ax,%fs# 因为修改了gdt,所以重新加载这些寄存器 mov %ax,%gs lss _stack_start,%esp ; 测试A20地址线是否开启 ; 方法:向内存地址0处写入任意一个数值,看内存地址是否是这个数值 ; 如果是就一直比较下去,产生死循环, ; 表示A20线没有选通,内核就不能使用1MB以后的内存空间 xorl %eax,%eax 1: incl %eax# check that A20 really IS enabled # 1-->标号,表示活动位置计数的当前值,并可以作为指令的操作数 movl %eax,0x000000 # loop forever if it isn't cmpl %eax,0x100000 je 1b# Nb:引用先前最近的标号 'b':backwards 向后 # Nf:引用下一个标号'f':forwards向前 # 这里‘1b’表示:向后跳转到标号1去 # 如果是5f,则是向前跳转到标号5去/* * NOTE! 486 should set bit 16, to check for write-protect in supervisor * mode. Then it would be unnecessary with the "verify_area()"-calls. * 486 users probably want to set the NE (#5) bit also, so as to use * int 16 for math errors. */ ; 检测486数学协处理器芯片是否存在 ; 方法:修改控制寄存器CR0,然后执行一条协处理器指令,若出错,则不存咋 ; 需要设置协处理器仿真位(EM)位2,并复位协处理器存在标志(MP)位2 movl %cr0,%eax# check math chip andl $0x80000011,%eax # Save PG,PE,ET /* "orl $0x10020,%eax" here for 486 might be good */ orl $2,%eax# set MP并复位协处理器存在标志(MP) movl %eax,%cr0 call check_x87 jmp after_page_tables/* * We depend on ET to be correct. This checks for 287/387. */ ; fninit和fstsw是协处理器的指令 check_x87: fninit# 向协处理器发出初始化指令 fstsw %ax# 将协处理器状态字复制给ax cmpb $0,%al# 初始化后状态字应该位0,否则协处理器不存在 je 1f/* no coprocessor: have to set bits */ #跳转到标号位1处(前面) movl %cr0,%eax# 如果存在则跳到标号位1处,否则改写cr0 xorl $6,%eax/* reset MP, set EM */ ; 设置协处理器仿真位(EM) movl %eax,%cr0 ret # .align 是汇编语言指示符,用于存储边界对齐调整 # 2表示把随后的代码和数据的偏移位置调整到地址值最后2比特位为0的位置(2*2) # 即按四字节方式对齐内存地址 # 使用该指示符目的是为了提高32位cpu访问内存中代码或数据的速度和效率 .align 2 # 287协处理码,将80287设置为保护模式 1: .byte 0xDB,0xE4/* fsetpm for 287, ignored by 387 */ ret/* *setup_idt * *sets up a idt with 256 entries pointing to *ignore_int, interrupt gates. It then loads *idt. Everything that wants to install itself *in the idt-table may do so themselves. Interrupts *are enabled elsewhere, when we can be relatively *sure everything is ok. This routine will be over- *written by the page tables. */ ; 设置中断描述符表(IDT)字程序setup_idt ; 将中断描述符表设置具有256个项,并都指向ignore_int中断门,然后加载中断描述符表寄存器 ; 中断描述符表中的项为8个字节,称为门描述符 setup_idt: lea ignore_int,%edx# 将ignore_int有效地址值赋值给edx movl $0x00080000,%eax # 将选择符0x0008赋值给eax的高16位 movw %dx,%ax/* selector = 0x0008 = cs */ movw $0x8E00,%dx /* interrupt gate - dpl=0, present */ lea _idt,%edi# _idt 是中断描述符表的地址 mov $256,%ecx rp_sidt: movl %eax,(%edi)# eax -> [edi] 将哑中断门描述符存入表中 movl %edx,4(%edi)# edx -> [edi+4] addl $8,%edi# edi + 8 -> edi dec %ecx jne rp_sidt lidt idt_descr#加载中断描述符表寄存器 ret/* *setup_gdt * *This routines sets up a new gdt and loads it. *Only two entries are currently built, the same *ones that were built in init.s. The routine *is VERY complicated at two whole lines, so this *rather long comment is certainly needed :-). *This routine will beoverwritten by the page tables. */ # 设置全局描述符表项 setup_gdt: lgdt gdt_descr# 加载全局描述符寄存器 ret/* * I put the kernel page tables right after the page directory, * using 4 of them to span 16 Mb of physical memory. People with * more than 16MB will have to expand this. */ # 将内核的内存页表直接放在页目录后,使用4个表来寻址16MB的物理地址 .org 0x1000# 从偏移地址0x1000处开始是第一个页表(偏移0开始将存放页表目录) pg0:.org 0x2000 pg1:.org 0x3000 pg2:.org 0x4000 pg3:.org 0x5000# 定义下面的内存数据从偏移地址0x5000开始/* * tmp_floppy_area is used by the floppy-driver when DMA cannot * reach to a buffer-block. It needs to be aligned, so that it isn't * on a 64kB border. */ # 当DMA(直接存储器访问)不能访问缓冲块时,则_tmp_floppy_area内存块就可以供软盘驱动程序使用 # 需要保证地址对齐 _tmp_floppy_area: .fill 1024,1,0# 保留1024项,每一项一个字节,填充数值为0# 下面几个入栈操作为了跳转到init/main.c中main()函数做准备 # 前面三个入栈0值分别表示envp,argv指针和argc的值 after_page_tables: pushl $0# These are the parameters to main :-)main函数参数 envp pushl $0# argv指针 pushl $0# argc pushl $L6# return address for main, if it decides to. 模拟调用main时首先将返回地址入栈的操作 pushl $_main# _main--->main jmp setup_paging# main函数到不了这里 ,到标号L6这里,是一个死循环 L6: jmp L6# main should never return here, but # just in case, we know what happens./* This is the default interrupt "handler" :-) */ # 下面是默认的中断'向量句柄'' int_msg: .asciz "Unknown interrupt\n\r"# 定义‘未知的指定’ .align 2# 按4字节方式对齐内存地址# 中断处理过程 ignore_int: pushl %eax pushl %ecx pushl %edx push %ds# 这里请注意ds,es,fs,gs等虽然是16位的寄存器 push %es# 但仍然会以32位的形式入栈,即需要占用4个字节的栈空间 push %fs# 以上用于保存寄存器 movl $0x10,%eax mov %ax,%ds mov %ax,%es mov %ax,%fs pushl $int_msg call _printk# 该函数在 kernel/printk.c 中 popl %eax# 清理参数 pop %fs pop %es pop %ds popl %edx popl %ecx popl %eax iret# 中断返回(把中断调用是压入栈的CPU标志寄存器值也弹出)/* * Setup_paging * * This routine sets up paging by setting the page bit * in cr0. The page tables are set up, identity-mapping * the first 16MB. The pager assumes that no illegal * addresses are produced (ie >4Mb on a 4Mb machine). * * NOTE! Although all physical memory should be identity * mapped by this routine, only the kernel page functions * use the >1Mb addresses directly. All "normal" functions * use just the lower 1Mb, or the local data space, which * will be mapped to some other place - mm keeps track of * that. * * For those with more memory than 16 Mb - tough luck. I've * not got it, why should you :-) The source is here. Change * it. (Seriously - it shouldn't be too difficult. Mostly * change some constants etc. I left it at 16Mb, as my machine * even cannot be extended past that (ok, but it was cheap :-) * I've tried to show which constants to change by having * some kind of marker at them (search for "16Mb"), but I * won't guarantee that's all :-( ) */ # 下面这段子程序通过控制CR0的标志位(PG位31)来启动对内存的分页处理功能,并设置各个表项的内容 .align 2# 按4字节方式对齐内存地址边界 setup_paging: movl $1024*5,%ecx/* 5 pages - pg_dir+4 page tables */#对(1页目录+4页页表)清零 xorl %eax,%eax xorl %edi,%edi/* pg_dir is at 0x000 */ #目录页从0x00开始 cld; rep; stosl # 设置页目录表中的项,内核中有4个页表需要设置4项 movl $pg0+7,_pg_dir/* set present bit/user r/w */# pg0+7:0x000010007,页目录表中的第一项 movl $pg1+7,_pg_dir+4/*--------- " " --------- */ movl $pg2+7,_pg_dir+8/*--------- " " --------- */ movl $pg3+7,_pg_dir+12/*--------- " " --------- */ # 填写4个页表中所有内容:4(页表)*1024(项)=4096(项) movl $pg3+4092,%edi movl $0xfff007,%eax/*16Mb - 4096 + 7 (r/w user,p) */ std 1: stosl/* fill pages backwards - more efficient :-) */ subl $0x1000,%eax jge 1b # 设置页目录表基址寄存器CR3的值(保存的页目录表的物理地址),指向页目录表, xorl %eax,%eax/* pg_dir is at 0x0000 */ movl %eax,%cr3/* cr3 - page directory start */ movl %cr0,%eax # 设置启用分页处理,(cr0的标志PG,位31) orl $0x80000000,%eax movl %eax,%cr0/* set paging (PG) bit */ ret/* this also flushes prefetch-queue */# 在改变分页处理标志后要求使用转移指令刷新预取指令队列,这里用的是返回指令ret # 返回指令的作用将main程序压入栈中的地址弹出,并转到init/main.c去运行 .align 2 .word 0# 加载中断描述符表寄存器idtr的lidt指令 idt_descr: .word 256*8-1# idt contains 256 entries .long _idt .align 2 .word 0# 加载全局描述符表寄存器gdtr的lgdt指令 gdt_descr: .word 256*8-1# so does gdt (not that that's any .long _gdt# magic number, but it works for me :^) .align 3_idt: .fill 256,8,0# idt is uninitialized# 全局表,前4项是空项,代码段描述符,数据段描述符,系统调用段描述符 _gdt: .quad 0x0000000000000000 /* NULL descriptor */ .quad 0x00c09a0000000fff /* 16Mb */ .quad 0x00c0920000000fff /* 16Mb */ .quad 0x0000000000000000 /* TEMPORARY - don't use */ .fill 252,8,0/* space for LDT's and TSS's etc */

    推荐阅读