linux0.1x内核代码学习笔记-boot启动

         linux0.11上电时把启动盘第1扇区bootsect.s的代码拷贝到0x7c00位置处,这段代码自己把自己拷贝到0x90000 这个位置然后开始执行,利用bios预先设置好的中断函数,把第2扇区setup程序拷贝到0x90200处,一共4个扇区。把第6扇区开始的240个扇区system代码读取到内存地址 0x10000处共120KB,整个操作系统的代码已经读取到内存了,然后再确定根文件设备保存到root_dev这个位置,跳转到setup程序运行。

 SYSSIZE = 3000h		;// 指编译连接后system模块的大小。
						;// 这里给出了一个最大默认值。

 SETUPLEN = 4			;// setup程序的扇区数(setup-sectors)值
 BOOTSEG  = 07c0h		;// bootsect的原始地址(是段地址,以下同)
 INITSEG  = 9000h		;// 将bootsect移到这里
 SETUPSEG = 9020h		;// setup程序从这里开始
 SYSSEG   = 1000h		;// system模块加载到10000(64kB)处.
 ENDSEG   = SYSSEG + SYSSIZE		;// 停止加载的段地址

; 设备号具体值的含义如下:
; 设备号=主设备号*256 + 次设备号(也即 dev_no = ( major <<8 ) + minor )
; (主设备号:1-内存,2-磁盘,3-硬盘,4-ttyx,5-tty,6-并行口,7-非命名管道)
; 0x300 - /dev/hd0 - 代表整个第 1 个硬盘;
; 0x301 - /dev/hd1 - 第 1 个盘的第 1 个分区;
; …
; 0x304 - /dev/hd4 - 第 1 个盘的第 4 个分区;
; 0x305 - /dev/hd5 - 代表整个第 2 个硬盘盘;
; 0x306 - /dev/hd6 - 第 2 个盘的第 1 个分区;
; …
; 0x309 - /dev/hd9 - 第 2 个盘的第 4 个分区;
; 从Linux内核0.95版后已经使用与现在相同的命名方法了。

! After that we check which root-device to use. If the device is
! defined (!= 0), nothing is done and the given device is used.
! Otherwise, either /dev/PS0 (2,28) or /dev/at0 (2,8), depending
! on the number of sectors that the BIOS reports currently.
; 此后,我们检查要使用哪个根文件系统设备(简称根设备)。如果已经指定了设备(!= 0)就直
; 接使用给定的设备。否则就需要根据 BIOS 报告的每磁道扇区数来确定到底使用/dev/PS0 (2,28)
; 还是 /dev/at0 (2,8)。
; 上面一行中两个设备文件的含义:
; 在 Linux 中软驱的主设备号是2(参见第 78 行的注释),次设备号 = type * 4 + nr,其中
; nr 为 0 - 3 分别对应软驱A、B、C 或D;type 是软驱的类型(2->1.2M 或 7->1.44M 等)。
; 因为 7*4 + 0 = 28,所以 /dev/PS0 (2,28)指的是1.44M A 驱动器,其设备号是 0x021c
; 同理 /dev/at0 (2,8)指的是1.2M A 驱动器,其设备号是0x0208。

    seg cs
    mov	ax,root_dev     ; 取出 root_dev 的值,判断根设备号是否被定义
    or	ax,ax
    jne	root_defined
    seg cs              ; 取出 sectors 的值(每磁道扇区数);sectors = 15 则说明是 1.2MB 的驱动器;
    mov	bx,sectors      ; sectors = 18 则说明是 1.44MB 的软驱。因为是可引导的驱动器,所以是A驱。
    mov	ax,#0x0208      ! /dev/PS0 - 1.2Mb
    cmp	bx,#15
    je	root_defined
    mov	ax,#0x021c      ! /dev/PS0 - 1.44Mb
    cmp	bx,#18
    je	root_defined
undef_root:             ; 都不等于的情况下则进入死循环
    jmp undef_root
root_defined:			; 将检查过的设备号保存到 root_dev 中。
    seg cs
    mov	root_dev,ax

! after that (everyting loaded), we jump to
! the setup-routine loaded directly after
! the bootblock:
; 到此,所有程序都加载完毕,我们就跳转到被加载在 bootsect 后面的 setup 程序去。

    jmpi	0,SETUPSEG
        .
        .
        .
        .
        .
sectors:
    .word 0

msg1:
    .byte 13,10
    .ascii "Loading"

.org 506
; 表示下面语句从地址506(0x1FC)开始,所以swap_dev在启动扇区的第506开
; 始的2个字节中,root_dev在启动扇区的第508开始的2个字节中。
swap_dev:
    .word SWAP_DEV
root_dev:
    .word ROOT_DEV

; 下面是启动盘具有有效引导扇区的标志。仅供BIOS中的程序加载引导扇区时识别使用.它必须位于引
; 导扇区的最后两个字节中。
boot_flag:
    .word 0xAA55

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

bootsect程序总结:

  1. 读取setup程序到0x90200处,一共4个扇区2K
  2. 读取系统代码到0x10000处,一共240扇区120KB
  3. 检查文件设备类型把设备号写到root_dev这个位置(508-510字节+0x90000)

setup程序说明:

        1.首先设置DS段寄存器为0x9000,去取硬件信息存到内存0x90000处共510字节

内存地址

字节

名称

描述

0x90000

2

光标位置

行,列

0x90002

2

扩展内存数

从1M开始计算 KB

0x90004

2

显示页面

0x90006

1

显示模式

0x90007

1

字符列数

0x90008

2

0x9000A

1

显示内存

0x9000B

1

显示状态

0x9000C

2

特性参数

.......

0x90080

16

硬盘参数表

第一个硬盘参数表

0x90090

16

硬盘参数表

第二个硬盘参数表

0x901FC

2

根设备号

根文件系统设备号

        2.关闭cpu中断,把system代码从0x10000移动到0x00000处,此时实模式的中断向量已经被system代码段覆盖。

! now we want to move to protected mode ...
; 这里开始,我们开始进入保护模式

	cli			! no interrupts allowed !		; 这里开始不允许任何中断

! first we move the system to it's rightful place
; 首先我们将system模块移到正确的位置。
; bootsect引导程序是将system模块读入到从0x10000(64k)开始的位置。由于当时假设
; system模块最大长度不会超过0x80000(512k),也即其末端不会超过内存地址0x90000,
; 所以bootsect会将自己移动到0x90000开始的地方,并把setup加载到它的后面。
; 下面这段程序的用途是再把整个system模块移动到0x00000位置,即把从0x10000到0x8ffff
; 的内存数据块(512k),整块地向内存低端移动了0x10000(64k)的位置。

	mov	ax,#0x0000
	cld					! 'direction' = 0, movs moves forward 	; 清方向位
do_move:
	mov	es,ax			! destination segment ; es:di是目的地址(初始为0x0:0x0)
	add	ax,#0x1000
	cmp	ax,#0x9000		; 已经把最后一段(从0x8000段开始的64KB)代码移动完.
	jz	end_move 		; 判断是否移动完成
	mov	ds,ax			! source segment
	sub	di,di
	sub	si,si
	mov cx,#0x8000 		; 移动 0x8000个字
	rep
	movsw
	jmp	do_move

        3.加载lgdt和lidt,打开A20地址实现1M内存以上的寻址,设置8259芯片的中断向量地址为0x20-0x28,跳转到代码段描述符1开始执行开始执行,也就是系统代码段执行。


; 全局描述符表开始处.描述符表由多个8字节长的描述符项组成.这里给出了3个描述符项.
; 第1项无用,但须存在.第2项的系统代码段描述符,第3项是系统数据段描述符.

gdt:
	.word	0,0,0,0		! dummy	;第1个描述符,不用

	; 在GDT表 这里的偏移量是0x08.它是内核代码段选择符的值.
	.word	0x07FF		! 8Mb - limit=2047 (2048*4096=8Mb)
	.word	0x0000		! base address=0
	.word	0x9A00		! code read/exec		; 代码段为只读,可执行
	.word	0x00C0		! granularity=4096, 386 ; 颗粒度4K,32位

	.word	0x07FF		! 8Mb - limit=2047 (2048*4096=8Mb)
	.word	0x0000		! base address=0
	.word	0x9200		! data read/write		; 数据段为可读可写
	.word	0x00C0		! granularity=4096, 386	; 颗粒度4K,32位

; 下面是加载中断描述符表寄存器idtr的指令lidt要求的6字节操作数.前2字节的IDT 的限长,后4字节是idt表在线性地址
; 空间中的32位基地址.CPU要求在进入保护模式之前需设置IDT表,因此这里先设置一个长度为0的空表.
idt_48:
	.word	0			! idt limit=0
	.word	0,0			! idt base=0L

; 加载全局描述符表寄存器 gdtr 的指令 lgdt 要求的 6 字节操作数。
gdt_48:
	.word	0x800		! gdt limit=2048, 256 GDT entries 	; 表限长2k,8个字节等于1个描述符,共有256项
	.word	512+gdt,0x9	! gdt base = 0X9xxxx 	; 0x90200 + gdt

setup.s代码段总结:

  1. 读取硬件参数覆盖以前的bootsect.s代码处(0x90000-0x90200)。
  2. 关闭中断,移动系统代码从0x10000到0x0000处(共拷贝内存512KB)。
  3. 设置idt gdt 打开A20地址实现1M以上寻址,初始化8259A中断向量基地址为0x20-0x28(因为cpu使用0x00-0x20中断为其他用处),跳转到系统代码运行。

中断请求号

中断号

用途

IRQ0

0x20(32)

8253发出的100HZ时钟中断

IRQ1

0x21(33)

键盘中断

IRQ2

0x22(34)

连接从芯片

IRQ3

0x23(35)

串行口2

IRQ4

0x24(36)

串行口1

IRQ5

0x25(37)

并行口2

IRQ6

0x26(38)

软盘驱动器

IRQ7

0x27(39)

并行口1

IRQ8

0x28(40)

实时时钟

IRQ9

0x29(41)

保留

IRQ9

0x2A(42)

保留

IRQ9

0x2B(43)

保留(网络接口)

IRQ9

0x2C(44)

PS/2鼠标接口

IRQ9

0x2D(45)

数学协处理器中断

IRQ9

0x2E(46)

硬盘中断

IRQ9

0x2F(47)

保留

head.s程序说明:

        1.head程序和系统c语言代码是分别编译链接到一起组成system模块,head在最前面占用大概25KB+184B字节空间,head程序首先初始化所有段寄存器,初始化堆栈地址指针,设置中断向量,设置gdt描述符,段限长有所改变,所以再次重新设置段寄存器清除段寄存器缓存,设置idt中断处理函数,统一设置成忽略中断打印函数,检查A20地址是否被打开(往0处写一个数再去0x100000处读取判断数值是否一致,不一致则A20已经打开)。

/*
 *  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.
 */
/*
 * 下面这段是设置中断描述符表子程序setup_idt
 *
 * 将中断描述符表idt设置成具有256个项,并都指向ignore_int中断门.然后加载中断描述符表寄存器(lidt指令)。
 * 真正实用的中断门以后再安装.当我们在其他地方认为一切都正常时再开启中断.该子程序将会被页表覆盖掉.
 */
# 中断描述符表中的项虽然也是8字节组成,但其格式与全局表中的不同,被称为门描述符.它的0-1,6-7字节是偏移量,
# 2-3字节是选择符,4-5字节是一些标志.该描述符,共256项.eax含有描述符低4字节,edx含有高4字节.内核在随后
# 的初始化过程中会替换安装那些真正实用的中断描述符项。

setup_idt:
    lea ignore_int, %edx		# 将ignore_int的有效地址(偏移值)值->eax寄存器
    movl $0x00080000, %eax		# 将选择符0x0008置入eax的高16位中.
    movw %dx, %ax				/* selector = 0x0008 = cs */			
                                # 偏移值的低16位置入eax的低16位中.此时eax含有门描述符低4字节的值。
    movw $0x8E00, %dx			/* interrupt gate - dpl=0, present */	
                                # 此时edx含有门描述符高4字节的值.

    lea idt, %edi				# idt是中断描述符表的地址.
    mov $256, %ecx
rp_sidt:
    movl %eax, (%edi)			# 将哑中断门描述符存入表中.
    movl %edx, 4(%edi)
    addl $8, %edi				# edi指向表中下一项.
    dec %ecx
    jne rp_sidt
    lidt idt_descr				# 加载中断描述符表寄存器值.
    ret


.align 4
ignore_int:
    pushl %eax
    pushl %ecx
    pushl %edx
    push %ds
    push %es
    push %fs
    
    movl $0x10, %eax				# 设置段选择符(使ds,es,fs指向gdt表中的数据段)
    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							# 中断返回

/*
 *  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
 * 这个子程序设置一个新的全局描述符表gdt,并加载.该子程序将被页表覆盖掉.
 *
 */
setup_gdt:
    lgdt gdt_descr	# 加载全局描述符表寄存器(内容已设置好)
    ret



# 下面是加载全局描述符表寄存器gdtr的指令lgdt要求的6字节操作数.前2字节是gdt表的限长,后4字节是gdt表的
# 线性基地址.这里全局表长度设置为2KB字节(0x7ff即可),因为每8字节组成一个描述符项,所以表中共可有256项。
# 符号gdt是全局表在本程序中的偏移位置.

gdt_descr:
    .word 256 * 8 - 1					# so does gdt (not that that's any
    .long gdt							# magic number, but it works for me :^)

    .align 8							# 按8(2^3)字节方式对齐内存地址边界.
idt:	.fill 256, 8, 0					# idt is uninitialized	# 256项,每项8字节,填0.

# 全局表,前4项分别是空项(不用),代码段描述符,数据段描述符,系统调用段描述符,其中系统调用段描述符并没有
# 派用处,Linus当时可能曾想把系统调用代码专门放在这个独立的段中。同时还预留了252项的空间,用于放置所创
# 建任务的局部描述符(LDT)和对应的任务状态段TSS的描述符.
# (0-nul, 1-cs, 2-ds, 3-syscall, 4-TSS0, 5-LDT0, 6-TSS1, 7-LDT1, 8-TSS2 etc...)
gdt:
    .quad 0x0000000000000000			/* NULL descriptor */
    .quad 0x00c09a0000000fff			/* 16Mb */		# 0x08,内核代码段最大长度16MB.
    .quad 0x00c0920000000fff			/* 16Mb */		# 0x10,内核数据段最大长度16MB.
    .quad 0x0000000000000000			/* TEMPORARY - don't use */
    .fill 252, 8, 0						/* space for LDT's and TSS's etc */	# 预留空间.

        2.检查协x87数学协处理器是否存在,芯片不存在,需要设置CR0中的协处理器仿真位EM(位2),并复位协处理器存在标志MP(位1)。

/*
 * 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应该将位16置位,以检查在超级用户模式下的写保护,此后"verify_area()"
 * 调用就不需要了。486的用户通常也会想将NE(#5)置位,以便对数学协处理器的出错使用int 16。
 *
 */
 # 上面原注释中提到的486CPU中CR0控制器的位16是写保护标志WP,用于禁止超级用户级的程序向一般用户只读
 # 页面中进行写操作。该标志主要用于操作系统在创建新进程时实现写时复制方法。

 # 下面这段程序用于检查数学协处理器芯片是否存在
 # 方法是修改控制寄存器CR0,在假设存在协处理器的情况下执行一个协处理器指令,如果出错的话则说明协处理器
 # 芯片不存在,需要设置CR0中的协处理器仿真位EM(位2),并复位协处理器存在标志MP(位1)。
    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
    movl %eax, %cr0
    call check_x87
    jmp after_page_tables

/*
 * We depend on ET to be correct. This checks for 287/387.
 */
/*
 * 我们依赖于ET标志的正确性来检测287/387存在与否.
 *
 */
# fninit向协处理器发出初始化命令,它会把协处理器置于一个末受以前操作影响的已和状态,设置其控制字为默认值,
# 清除状态字和所有浮点栈式寄存器。非等待形式的这条指令(fninit)还会让协处理器终止执行当前正在执行的任何
# 先前的算术操作。
# fstsw指令取协处理器的状态字。
# 如果系统中存在协处理器的话,那么在执行了fninit指令后其状态字低字节肯定为0。

check_x87:
    fninit
    fstsw %ax
    cmpb $0, %al						# 初始化状态字应该为0,否则说明协处理器不存在
    je 1f								/* no coprocessor: have to set bits */
    movl %cr0, %eax
    xorl $6, %eax						/* reset MP, set EM */
    movl %eax, %cr0
    ret

.align 4 # 按4字节方式对齐内存地址, 为了提高32位CPU访问内存中代码或数据的速度和效率
    # 两个字节值是80287协处理器指令fsetpm的机器码。其作用是把80287设置为保护模式。
    # 80387无需该指令,并且将会把该指令看作是空操作
1:	.byte 0xDB,0xE4		/* fsetpm for 287, ignored by 387 */	# 287协处理器码
    ret

        3.设置页目录和页表,然后返回到main函数开始运行,页目录一共设置了4项,页表也设置了4项,映射了16M地址线性地址与物理地址16M相对应映射。这里可能没有16m物理地址,只是页表实现了映射而已。


/***** 为跳转到init/main.c中的main()函数作准备工作 *****/
# 前面3个入栈0值应该分别表示envp,argv指针和argc的值(main()没有用到)
# pushl $L6    压入返回地址
# pushl $main  压入main函数的入口地址
# 当head.s最后执行ret指令时就会弹出main()的地址
after_page_tables:
    pushl $0						# These are the parameters to main :-)
    pushl $0						# 这些是调用main程序的参数(指init/main.c).
    pushl $0
    pushl $L6						# return address for main, if it decides to.
    pushl $main
    jmp setup_paging				# 跳转至setup_paging
L6:
    jmp L6							# main should never return here, but
                                    # just in case, we know what happens.
                                    # main程序绝对不应该返回到这里,不过为了以防万一,所以
                                    # 添加了该语句。这样我们就知道发生什么问题了。


/*
 * 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)来启动对内存的分页处理功能,并设置各个页表项
 * 的内容,以恒等映射前16MB的物理内存。分页器假定不会产生非法的地址映射(也即在只有4MB的机器上
 * 设置出大于4MB的内存地址)
 *
 * 注意!尽管所有的物理地址都应该由这个子程序进行恒等映射,但只有内核页面管理函数能直接使用>1MB
 * 的地址。所有"普通"函数仅使用低于1MB的地址空间,或者是使用局部数据空间,该地址空间将被映射到
 * 其他一些地方去--mm(内存管理程序)会管理这些事的.
 *
 */
 # 上面英文注释第2段的含义是指在机器物理内存中大于1MB的内存空间主要被用于主内存区。主内存区空间
 # 由mm模块管理,它涉及页面映射操作。内核中所有其它函数就是这里指的"普通"函数。


# 初始化页目录表前4项和4个页表
.align 4
setup_paging:
    movl $1024 * 5, %ecx				/* 5 pages - pg_dir+4 page tables */
    xorl %eax, %eax
    xorl %edi, %edi						/* pg_dir is at 0x000 */	
                                        # 页目录从0x0000地址开始
    cld;rep;stosl						# eax内容存到es:edi所指内存位置处,且edi增4.

     # 设置页目录表中的前4个页目录项
     # 例如第1个页目录项:
     #      页表所在地址 = 0x00001007 & 0xfffff000 = 0x1000
     #      页表属性标志 = 0x00001007 & 0x00000fff = 0x07    表示该页存在,用户可读写.
    movl $pg0 + 7, pg_dir				/* set present bit/user r/w */
    movl $pg1 + 7, pg_dir + 4			/*  --------- " " --------- */
    movl $pg2 + 7, pg_dir + 8			/*  --------- " " --------- */
    movl $pg3 + 7, pg_dir + 12			/*  --------- " " --------- */

    # 设置4个页表中所有项的内容(共4096项),从最后一个页表的最后一项开始按倒退顺序填写
    movl $pg3 + 4092, %edi			# edi->最后一页的最后一项.
    movl $0xfff007, %eax			/* 16Mb - 4096 + 7 (r/w user,p) */
    std								# 方向位置位,edi值递减(4字节)
1:	stosl							/* fill pages backwards - more efficient :-) */
    subl $0x1000, %eax				# 每填好一项,物理地址值减0x1000。
    jge 1b							# 如果小于0则说明全填写好了
    cld
    # 设置页目录表基地址寄存器cr3(保存页目录表的物理地址)
    xorl %eax, %eax					/* pg_dir is at 0x0000 */
    movl %eax, %cr3					/* cr3 - page directory start */
    # 设置启动使用分页处理(cr0的PG标志,位31)
    movl %cr0, %eax
    orl $0x80000000, %eax			# 添上PG标志
    movl %eax, %cr0					/* set paging (PG) bit */
    ret								/* this also flushes prefetch-queue */

# 在改变分页处理标志后要求使用转移指令刷新预取指令队列,这里用的是返回指令ret。
# 该返回指令ret的另一个作用并跳转到/init/main.c程序去运行。

head.s程序总结:

  1. 设置了GDT和IDT ,检查A20地址,重新初始化段寄存器,特别注意设置的堆栈地址。
  2. 检查x87处理器是否存在设置仿真标志位,复位协处理器存在MP位。
  3. 压栈main可能使用的参数,设置16M的页表映射,然后跳转到main函数执行。

boot总结:

        读取setup到0x90200和system代码到0x10000,设置根设备号到内存0x90000+508跳转到steup运行,setup读取硬件参数到0x90000-0x90200,关闭cpu中断,初始化8259A芯片,设置idt(限制和基地址都设为0)和gdt描述符(1个代码段,1个数据段都是(0-8M字节)),打开A20地址实现1M以上地址的寻址,打开保护模式,跳转到地址0运行head的代码,head代码重新设置了GDT代码限制改为16M,数据段也改为16M都从0-16M地址,一共就2段,idt设置256个一样的中断门都打印一样的数据,设置堆栈为c语言定义的stack_start,4K字节,设置DS等指向数据段,这里重新设置了几次主要是修改了gdt需要刷新段寄存器隐藏的部分缓存,最后设置页表页目录覆盖从0开始的部分代码,实现的映射16M对应物理地址16M没做转换,最后打开分页管理,跳转到main函数执行,这里使用提前压栈的main用的ret指令执行main函数。

        现在操作系统已经有了简单的中断处理函数,页管理16M,代码段描述符和数据段描述符都为16M,描述符基地址限制是一样的只是属性不同,段寄存器都指向的数据段描述符,esp指向的stack_start,硬件参数存放到0x90000-0x90000+510,然后返回到压栈的c语言的main函数开始运行。

参考资料:

《Linux内核设计的艺术》【作者:新设计团队】

《Linux内核完全注释》 【作者:赵炯】

《Orange‘s一个操作系统的实现》【作者:于渊】

《x86汇编语言-从实模式到保护模式》【作者:李忠】

https://github.com/1358484518/linux0.11-master

你可能感兴趣的:(linux内核代码学习笔记,gnu,操作系统,linux,c语言,架构)