您的位置:首页 > 运维架构 > Linux

linux-1.2.13之启动代码head.S

2015-07-04 21:31 681 查看

1 概述

       本文主要分析linux-1.2.13的启动文件head.S。在linux内核前面的这一段代码head.s主要用来设置linux初始运行环境,为linux内核的运行做前期准备。

       x86参考:http://css.csail.mit.edu/6.858/2015/readings/i386.pdf

2 代码分析

         下面开始分析启动代码:

startup_32:
cld
movl $(KERNEL_DS),%eax /*初始化各个段寄存器*/
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
mov %ax,%gs
lss stack_start,%esp   /*将堆栈指向stack_start, 该变量定义在misc.c中*/


         刚开始进入程序,首先将各个段寄存器进行初始化,然后设置栈指针。

xorl %eax,%eax     /*eax清零*/
movl $__edata,%edi /*BSS的起始地址放入edi*/
movl $__end,%ecx   /*BSS的结束地址放入ecx*/
subl %edi,%ecx     /*起始地址减去结束地址得到BSS段的长度*/
cld                /*每操作一次,edi的值增加*/
rep                /*重复下面的指令直到ecx==0*/
stosb              /*将eax放入edi指向的地址中,然后将edi的值加上4*/


        初始化段寄存器和栈指针之后,开始将系统中的BSS段清零。BSS段为未初始化数据段,由编译器分配。上面汇编代码中的_edata和_end是由ld链接器生成的,源代码中只需要引用就可以知道BSS段的起始和结束地址。

call setup_idt     /*调用函数setup_idt初始化中断向量表*/
xorl %eax,%eax     /*eax清零*/
1:	incl %eax          /*eax值加一*/
movl %eax,0x000000 /*将eax放入地址0x00处*/
cmpl %eax,0x100000 /*检查0x100000处的内容是否与eax 相同*/
je 1b              /*相同则处于实模式,跳至1处死循环*/
        系统运行至此处应该已经处于保护模式,这段代码就是用来检查当前系统是否处于保护模式,如果不处于保护模式则进入死循环。检测原理是如果系统处于实模式,则地址0x000000和0x1000000应该指的是同一块内存,这样在其中一个地方写一个标志在另外一个地方检查,如果相同则是实模式,不相同则处于保护模式。

movl $0x90000,%esi   /*将0x90000开始2k字节的内容放到empty_sero_size处*/
movl $_empty_zero_page,%edi
movl $512,%ecx
cld
rep
       在0x90000处存放了bootloader从系统中获取的一些信息,此处将存储在0x90000处的系统信息放入empty_zero_page处。empty_zero_page是系统分配的一块存放系统信息的区域,处于0x50000处。
2:	movl %eax,%cr0
call check_x87      /*调用函数check_x87检查是否存在协处理器*/
call setup_paging   /*设置页目录*/
lgdt gdt_descr      /*加载全局描述表寄存器GDTR*/
lidt idt_descr      /*加载中断描述符表寄存器IDTR*/
ljmp $(KERNEL_CS),$1f   /*转跳至标号1处*/
1:	movl $(KERNEL_DS),%eax  /*重新加载所有的段寄存器,开始使用新的GDT*/
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
mov %ax,%gs
lss stack_start,%esp    /*ss赋值为KERNEL_DS,栈的指针指向stack_start,stack_start定义在文件misc.c中*/
xorl %eax,%eax
lldt %ax                /*加载LDTR寄存器*/
pushl %eax		/*将start_kernel的三个参数压入栈中*/
pushl %eax
pushl %ex
cld
call _start_kernel      /*启动内核*/
L6:
jmp L6
       在进入内核之前,还会检查是否有协处理器,设置页目录,栈指针等,最后通过call _start_kernel进入内核。



setup_idt:
lea ignore_int,%edx /*将ignore_init的偏移地址放入edx中 */
movl $(KERNEL_CS << 16),%eax /*构建中断门中的段选择子*/
movw %dx,%ax /* selector = 0x0010 = cs 构建中断门的偏移地址*/
movw $0x8E00,%dx /* 设置中断门的DPL=0 */

lea _idt,%edi /*将IDT表的地址放入edi*/
mov $256,%ecx /*IDT表中一共有256个中断描述符*/
rp_sidt:
movl %eax,(%edi) /*填充中断向量描述符中的段段选择子和偏移地址*/
movl %edx,4(%edi) /*填充中断向量描述符中的DPL等属性*/
addl $8,%edi /*填充下一个描述符*/
dec %ecx /*计数减一*/
jne rp_sidt /*直到把整个IDT表填充完毕*/
ret
       该数据块用来填充设置中断描述符表IDT,将所有的中断函数都设置为ignore_int(),该函数只会打印出一条字符串,剩下的什么都不做。





setup_paging:
movl $1024*2,%ecx             /*将swapper_pg_dir和pg0这个两个pages清零*/
xorl %eax,%eax                /*swapper_pg_dir为一级页目录,pg0为二级页目录*/
movl $_swapper_pg_dir,%edi
cld;rep;stosl

movl $_pg0+7,_swapper_pg_dir		/*将pg0映射到线性地址0x0000 0000 ~ 0x0040 0000*/
movl $_pg0+7,_swapper_pg_dir+3072	/*将pg0映射到线性地址0xC000 0000 ~ 0xC020 0000*/

/*初始化pg0页表,将其与实际的物理地址对应起来,pg0与0~4M物理地址对应起来*/
movl $_pg0+4092,%edi          /*edi指向页表pg0的最后一个页表项*/
movl $0x03ff007,%eax          /*初始化页表项:物理起始地址=0x3FF000(4M-1K),flag=(R/W user, P)*/
std
1:	stosl			      /*将pg0页表逐项初始化*/
subl $0x1000,%eax
jge 1b
cld

movl $_swapper_pg_dir,%eax    /*将页目录表的地址放入eax*/
movl %eax,%cr3        	      /*CR3指向页目录表的开始处*/
movl %cr0,%eax                /*打开分页机制*/
orl $0x80000000,%eax
movl %eax,%cr0		      /*设置分页标志位PG*/
ret

       分页程序将线性地址0x0000 0000~0x0040 0000和0xC000 0000~0xC020 0000都映射到页表pg0上面,pg0又将地址映射到物理地址0x0000 0000~0x0040 0000(0~4M)上面。也就是说linux内核工作在两个线性地址处,这两块线性空间最后都映射到同一块物理内存。

     linux将内存空间分为内核空间和用户空间,其中linux内核空间大小为1GB,DPL为0,用户空间大小为3GB,DPL为3.





.align 4
_gdt:
.quad 0x0000000000000000 /* NULL descriptor */
.quad 0x0000000000000000 /* not used */
.quad 0xc0c39a000000ffff /* 0x10 kernel 1GB code at 0xC0000000 */
.quad 0xc0c392000000ffff /* 0x18 kernel 1GB data at 0xC0000000 */
.quad 0x00cbfa000000ffff /* 0x23 user 3GB code at 0x00000000 */
.quad 0x00cbf2000000ffff /* 0x2b user 3GB data at 0x00000000 */
.quad 0x0000000000000000 /* not used */
.quad 0x0000000000000000 /* not used */
.fill 2*NR_TASKS,8,0 /* space for LDT's and TSS's etc */
       head.S初始化完毕跳入linux内核之前之后,系统的环境如下所示:



head.S只是做了一个最基本的运行环境配置,后续的配置会在linux内核中继续。

3 参考

下面是整个head.S的完整代码,附上以供参考:

/*
* linux/arch/i386/head.S
*
* Copyright (C) 1991, 1992 Linus Torvalds
*/

/*
* head.S contains the 32-bit startup code.
*/

.text
.globl _idt,_gdt,
.globl _swapper_pg_dir,_pg0
.globl _empty_bad_page
.globl _empty_bad_page_table
.globl _empty_zero_page
.globl _floppy_track_buffer

#define __ASSEMBLY__
#include <linux/tasks.h>
#include <linux/fd.h>
#include <asm/segment.h>

#define CL_MAGIC_ADDR 0x90020
#define CL_MAGIC 0xA33F
#define CL_BASE_ADDR 0x90000
#define CL_OFFSET 0x90022

/*
* swapper_pg_dir is the main page directory, address 0x00001000 (or at
* address 0x00101000 for a compressed boot).
*/
startup_32:
cld
movl $(KERNEL_DS),%eax /*初始化各个段寄存器*/
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
mov %ax,%gs
lss stack_start,%esp /*将堆栈指向stack_start, 该变量定义在misc.c中*/
/*
* Clear BSS first so that there are no surprises...
*//*清除BSS段,_edata是为初始化数据区的起始地址,_end是未初始化数据区的末尾地址,
它们都不是在代码中定义的,由ld链接器定义*/
xorl %eax,%eax /*eax清零*/
movl $__edata,%edi /*BSS的起始地址放入edi*/
movl $__end,%ecx /*BSS的结束地址放入ecx*/
subl %edi,%ecx /*起始地址减去结束地址得到BSS段的长度*/
cld /*每操作一次,edi的值增加*/
rep /*重复下面的指令直到ecx==0*/
stosb /*将eax放入edi指向的地址中,然后将edi的值加上4*/
/*
* start system 32-bit setup. We need to re-do some of the things done
* in 16-bit mode for the "real" operations.
*//*下面的代码主要是验证保护模式是否打开,如果处于实模式的话0x000000和*/
/*0x100000指向同一个地方,如果处于保护模式则指向不同的地方*/
call setup_idt /*调用函数setup_idt初始化中断向量表*/
xorl %eax,%eax /*eax清零*/
1: incl %eax /*eax值加一*/# check that A20 really IS enabled
movl %eax,0x000000 /*将eax放入地址0x00处*/ # loop forever if it isn't
cmpl %eax,0x100000 /*检查0x100000处的内容是否与eax 相同*/
je 1b/*相同则处于实模式,跳至1处死循环*/
/*
* Initialize eflags. Some BIOS's leave bits like NT set. This would
* confuse the debugger if this code is traced.
* XXX - best to initialize before switching to protected mode.
*/
pushl $0 /*将0压入栈中*/
popfl /*将栈中的0推出到eflag中,初始化eflag*/
/*
* Copy bootup parameters out of the way. First 2kB of
* _empty_zero_page is for boot parameters, second 2kB
* is for the command line.
*/
/*将0x90000处2k字节的信息复制到empty_zero_page处
,bootloader启动时将获取的信息存放在0x90000处了*/
movl $0x90000,%esi /*将0x90000开始2k字节的内容放到empty_sero_size处*/
movl $_empty_zero_page,%edi
movl $512,%ecx
cld /*sdi,edi增加*/
rep /*重复下面的命令至ecx=0*/
movsl /*将sdi指向的4字节复制到edi处,然后edi和sdi增加4*/

/*将empty_zero_page+2048处开始的2k字节清零*/
xorl %eax,%eax
movl $512,%ecx
rep
stosl /*将eax中的内容放至edi处,然后edi增加4*/

/*通过检查放在0x90020 处的魔术字来确认bootloader存放的信息中是否有命令行*/
cmpw $(CL_MAGIC),CL_MAGIC_ADDR
jne 1f /*无命令行信息则跳至标号处1*/

/*将命令行信息放入empty_zero_page+2048处*/
movl $_empty_zero_page+2048,%edi
movzwl CL_OFFSET,%esi /*存放命令行信息的偏移地址*/
addl $(CL_BASE_ADDR),%esi /*esi中存放命令行信息的线性地址*/
movl $2048,%ecx /*复制2048 bytes*/
rep
movsb
1:
/* check if it is 486 or 386. */
/*
* XXX - this does a lot of unnecessary setup. Alignment checks don't
* apply at our cpl of 0 and the stack ought to be aligned already, and
* we don't need to preserve eflags.
*/
/*检查机器是i386还是i486,i486机器的eflag标志中有AC位,i386没有.因此*/
/*可以通过向AC位写1然后读出来判断机器类型*/
movl $3,_x86 /*setup.c中定义的变量,存储cpu型号,取值3或4代表i386或i486*/
pushfl /*将EFLAGS标志压入栈中*/# push EFLAGS
popl %eax /*将栈中的RFLAGS推出到eax中*/# get EFLAGS
movl %eax,%ecx /*将EFLAGS保存到ecx中*/# save original EFLAGS
xorl $0x40000,%eax /*将AC标志取反*/# flip AC bit in EFLAGS
pushl %eax /*将取反AC标志的EFLAGS压入栈中*/# copy to EFLAGS
popfl /*将栈中的EFLAGS推出到EFLAGS中*/# set EFLAGS
pushfl /*把EFLAGS压入栈中*/# get new EFLAGS
popl %eax /*把栈中的EFLAGS推出到eax中*/# put it in eax
xorl %ecx,%eax /*找出当前EFLAGS与之前的区别*/# change in flags
andl $0x40000,%eax /*检查AX位*/ # check if AC bit changed
je is386 /*AC位为0则为i386*/
movl $4,_x86 /*本cpu为i486*/
movl %ecx,%eax /*将原始EFLAGS放入eax中*/
xorl $0x200000,%eax /*将check ID标志位置一*/# check ID flag
pushl %eax /*将eax压入栈中*/
popfl /*将eax放入EFLAGS中*/# if we are on a straight 486DX, SX, or
pushfl /*将EFLAGS压入栈中*/# 487SX we can't change it
popl %eax /*将栈中的EFLAGS推出到eax中*/
xorl %ecx,%eax /*找出当前EFLAGS与原始EFLAGS之间的区别*/
andl $0x200000,%eax /*查看check ID标志位*/
je is486 /*如果check ID标志位为0表示机器是i486*/
isnew: pushl %ecx /*将原始EFLAGS压入栈中*/# restore original EFLAGS
popfl /*将EFLAGS恢复初始值*/
/* get processor type */
movl $1, %eax /*将参数放入eax,调用获取CPUID命令来获取处理器类型*/# Use the CPUID instruction to
.byte 0x0f, 0xa2 /*调用获取CPUID命令,该命令直接用机器码实现,返回值放入ax和edx*/# check the processor type
movb %al, %cl /*将al暂时保存到cl中*/# save reg for future use
andb $0x0f,%ah /*ah低4bits保存了处理器类型*/# mask processor family
movb %ah, _x86 /*将处理器型号放入x86变量*/
andb $0xf0, %eax /*al高4bits保存了cpu具体型号*/# mask model
shrb $4, %al /*将al的高4bits移位至底4bits处*/
movb %al, _x86_model/*将CPU具体型号放入x86_model变量中*/
andb $0x0f, %cl /*cl保存了原来的al值,al中的低4bits保存了mask revision*/# mask mask revision
movb %cl, _x86_mask /*将al中的mask revision放入x86_mask变量中*/
movl %edx, _x86_capability/*将CPU的一些信息放入x86_capability中*/
/* get vendor info */
xorl %eax, %eax /*eax中放入获取CPUID命令的参数,返回生产厂商ID*/
# call CPUID with 0 -> return vendor ID
.byte 0x0f, 0xa2 /*调用获取CPUID命令*/ # CPUID
movl %ebx, _x86_vendor_id /*将生产厂商信息放入x86_vender_id*/# lo 4 chars
movl %edx, _x86_vendor_id+4 # next 4 chars
movl %ecx, _x86_vendor_id+8 # last 4 chars

movl %cr0,%eax /*设置CR0中的AM,WP,NE,MP*/# 486+
andl $0x80000011,%eax # Save PG,PE,ET
orl $0x50022,%eax # set AM, WP, NE and MP
jmp 2f
is486: pushl %ecx /*将EFLAGS恢复原始值*/# restore original EFLAGS
popfl
movl %cr0,%eax /*设置CR0中的AM,WP,NE,MP*/# 486
andl $0x80000011,%eax # Save PG,PE,ET
orl $0x50022,%eax # set AM, WP, NE and MP
jmp 2f
is386: pushl %ecx /*将EFLAGS恢复到初始值*/# restore original EFLAGS
popfl
movl %cr0,%eax /*设置CR0中的MP*/# 386
andl $0x80000011,%eax # Save PG,PE,ET
orl $2,%eax # set MP
2: movl %eax,%cr0
call check_x87 /*调用函数check_x87检查是否存在协处理器*/
call setup_paging /*设置页目录*/
lgdt gdt_descr /*加载全局描述表寄存器GDTR*/
lidt idt_descr /*加载中断描述符表寄存器IDTR*/
ljmp $(KERNEL_CS),$1f /*转跳至标号1处*/
1: movl $(KERNEL_DS),%eax /*重新加载所有的段寄存器,开始使用新的GDT*/
mov %ax,%ds # after changing gdt.
mov %ax,%es
mov %ax,%fs
mov %ax,%gs
lss stack_start,%esp /*ss赋值为KERNEL_DS,栈的指针指向stack_start,stack_start定义在文件misc.c中*/
xorl %eax,%eax
lldt %ax /*加载LDTR寄存器*/
pushl %eax # These are the parameters to main :-)
pushl %eax
pushl %eax
cld # gcc2 wants the direction flag cleared at all times
call _start_kernel
L6:
jmp L6 # main should never return here, but
# just in case, we know what happens.

/*
* We depend on ET to be correct. This checks for 287/387.
*/
/*判断CPU是否存在协处理器*/
check_x87:
movb $0,_hard_math/*将0放入变量hard_match中表示不存在协处理器*/
clts /*清除CR0种的任务切换标志*/
fninit /*调用fninit命令向协处理器发出初始化命令*/
fstsw %ax /*将协处理器的状态字放入eax中*/
cmpb $0,%al /*状态字为0表示不存在协处理器*/
je 1f /*存在协处理器,跳至1处*/
movl %cr0,%eax /*不存在协处理器,需要将CR0中的协处理器仿真位EM置一*/
xorl $4,%eax /*设置EM位*//* set EM */
movl %eax,%cr0 /*设置CR0中的EM位*/
ret
.align 2
1: movb $1,_hard_math /*将1放入hard_math变量中表示存在协处理器*/
.byte 0xDB,0xE4 /*设置协处理器80287进入保护模式*/
/* fsetpm for 287, ignored by 387 */
ret

/*
* setup_idt
*
* sets up a idt with 256 entries pointing to
* ignore_int, interrupt gates. It doesn't actually load
* idt - that can be done only after paging has been enabled
* and the kernel moved to 0xC0000000. Interrupts
* are enabled elsewhere, when we can be relatively
* sure everything is ok.
*/
setup_idt:
lea ignore_int,%edx /*将ignore_init的偏移地址放入edx中 */
movl $(KERNEL_CS << 16),%eax /*构建中断门中的段选择子*/
movw %dx,%ax /* selector = 0x0010 = cs 构建中断门的偏移地址*/
movw $0x8E00,%dx /* interrupt gate - dpl=0, present中断门的DPL=0 */

lea _idt,%edi /*将IDT表的地址放入edi*/
mov $256,%ecx /*IDT表中一共有256个中断描述符*/
rp_sidt:
movl %eax,(%edi) /*填充中断向量描述符中的段段选择子和偏移地址*/
movl %edx,4(%edi) /*填充中断向量描述符中的DPL等属性*/
addl $8,%edi /*填充下一个描述符*/
dec %ecx /*计数减一*/
jne rp_sidt
ret

/*
* Setup_paging
*
* This routine sets up paging by setting the page bit
* in cr0. The page tables are set up, identity-mapping
* the first 4MB. The rest are initialized later.
*
* (ref: added support for up to 32mb, 17Apr92) -- Rik Faith
* (ref: update, 25Sept92) -- croutons@crunchy.uucp
* (ref: 92.10.11 - Linus Torvalds. Corrected 16M limit - no upper memory limit)
*/
.align 2/*页面大小固定为4k*/
setup_paging:
movl $1024*2,%ecx /*将swapper_pg_dir和pg0这个两个pages清零*/
xorl %eax,%eax
movl $_swapper_pg_dir,%edi /* swapper_pg_dir is at 0x1000 */
cld;rep;stosl

/* Identity-map the kernel in low 4MB memory for ease of transition */
movl $_pg0+7,_swapper_pg_dir /*将pg0映射到线性地址0x0000 0000 ~ 0x0040 0000处*/
/* set present bit/user r/w */
/* But the real place is at 0xC0000000 */
movl $_pg0+7,_swapper_pg_dir+3072 /*将pg0映射到线性地址0xC000 0000 ~ 0xC020 0000*/
/* set present bit/user r/w */

/*初始化pg0页表,将其与实际的物理地址对应起来,pg0与0~4M物理地址对应起来*/
movl $_pg0+4092,%edi /*edi指向页表pg0的最后一个页表项*/
movl $0x03ff007,%eax /*初始化页表项:物理起始地址=0x3FF000,flag=(R/W user, P)*/
/* 4Mb - 4096 + 7 (r/w user,p) */
std
1: stosl /*将pg0页表逐项初始化*//* fill the page backwards - more efficient :-) */
subl $0x1000,%eax
jg
a10c
e 1b
cld

movl $_swapper_pg_dir,%eax /*将页目录表的地址放入eax*/
movl %eax,%cr3 /*CR3指向页目录表的开始处*//*cr3-page directory start */
movl %cr0,%eax /*打开分页机制*/
orl $0x80000000,%eax
movl %eax,%cr0 /*设置分页标志位PG*//* set paging (PG) bit */
ret /* this also flushes the prefetch-queue */

/*
* page 0 is made non-existent, so that kernel NULL pointer references get
* caught. Thus the swapper page directory has been moved to 0x1000
*
* XXX Actually, the swapper page directory is at 0x1000 plus 1 megabyte,
* with the introduction of the compressed boot code. Theoretically,
* the original design of overlaying the startup code with the swapper
* page directory is still possible --- it would reduce the size of the kernel
* by 2-3k. This would be a good thing to do at some point.....
*/
.org 0x1000
_swapper_pg_dir:
/*
* The page tables are initialized to only 4MB here - the final page
* tables are set up later depending on memory size.
*/
.org 0x2000
_pg0:

.org 0x3000
_empty_bad_page:

.org 0x4000
_empty_bad_page_table:

.org 0x5000
_empty_zero_page:

.org 0x6000
/*
* floppy_track_buffer is used to buffer one track of floppy data: it
* has to be separate from the tmp_floppy area, as otherwise a single-
* sector read/write can mess it up. It can contain one full cylinder (sic) of
* data (36*2*512 bytes).
*/
_floppy_track_buffer:
.fill 512*2*MAX_BUFFER_SECTORS,1,0

stack_start:
.long _init_user_stack+4096
.long KERNEL_DS

/* This is the default interrupt "handler" :-) */
int_msg:
.asciz "Unknown interrupt\n"
.align 2
ignore_int:
cld
pushl %eax
pushl %ecx
pushl %edx
push %ds
push %es
push %fs
movl $(KERNEL_DS),%eax
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
pushl $int_msg
call _printk
popl %eax
pop %fs
pop %es
pop %ds
popl %edx
popl %ecx
popl %eax
iret

/*
* The interrupt descriptor table has room for 256 idt's
*/
.align 4
.word 0
idt_descr:
.word 256*8-1 # idt contains 256 entries
.long 0xc0000000+_idt

.align 4
_idt:
.fill 256,8,0 # idt is uninitialized

.align 4
.word 0
gdt_descr:
.word (8+2*NR_TASKS)*8-1
.long 0xc0000000+_gdt

/*
* This gdt setup gives the kernel a 1GB address space at virtual
* address 0xC0000000 - space enough for expansion, I hope.
*/
.align 4
_gdt:
.quad 0x0000000000000000 /* NULL descriptor */
.quad 0x0000000000000000 /* not used */
.quad 0xc0c39a000000ffff /* 0x10 kernel 1GB code at 0xC0000000 */
.quad 0xc0c392000000ffff /* 0x18 kernel 1GB data at 0xC0000000 */
.quad 0x00cbfa000000ffff /* 0x23 user 3GB code at 0x00000000 */
.quad 0x00cbf2000000ffff /* 0x2b user 3GB data at 0x00000000 */
.quad 0x0000000000000000 /* not used */
.quad 0x0000000000000000 /* not used */
.fill 2*NR_TASKS,8,0 /* space for LDT's and TSS's etc */
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: