linux0.11源码 - 启动项


硬盘里放着啥

  1. 把bootsect.s编译成 bootsect 放在硬盘的 1 扇区。bootsect.s负责各种从硬盘到内存的加载,以及内存到内存的复制。
  2. 把setup.s编译成setup放在硬盘的 2~5 扇区。
  3. 把剩下的全部代码(head.s 作为开头)编译成system放在硬盘的随后240个扇区。

bootsect.s文件:加载操作系统文件

SETUPLEN = 4                ! nr of setup-sectors
BOOTSEG  = 0x07c0            ! original address of boot-sector
INITSEG  = 0x9000            ! we move boot here - out of the way
SETUPSEG = 0x9020            ! setup starts here
SYSSEG   = 0x1000            ! system loaded at 0x10000 (65536).
ENDSEG   = SYSSEG + SYSSIZE        ! where to stop loading

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
! put stack at 0x9ff00.
mov    ss,ax
mov    sp,#0xFF00        
! arbitrary value >>512

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+SETUPLEN    ! service 2, nr of sectors
int    0x13            ! read it
jnc    ok_load_setup        ! ok - continue
mov    dx,#0x0000
mov    ax,#0x0000        ! reset the diskette
int    0x13
j    load_setup
! 省略非主逻辑代码
......
mov    ax,#SYSSEG
mov    es,ax        ! segment of 0x010000
call    read_it
......
! after that (everyting loaded), we jump to
! the setup-routine loaded directly after
! the bootblock:

jmpi    0,SETUPSEG
  1. BIOS 将操作系统代码加载到内存0x7c00,通过mov指令将默认的数据段寄存器ds寄存器的值改为0x07c0方便以后进行基址寻址。

    • mov ax,#BOOTSEG:将0x07c0复制到ax寄存器中。
    • mov ds,ax:将ax的值复制到ds段寄存器中,ds是一个16位的段寄存器,具体表示数据段寄存器,在内存寻址时充当段基址的作用。当我们之后用汇编语言写一个内存地址时,其实只写了偏移地址,实际地址=ds的值+偏移地址。
  2. 将内存地址0x7c00处开始往后的512字节的数据,原封不动复制到0x90000处。

    • mov ax,#INITSEG:将0x9000复制到ax寄存器中。
    • mov es,ax:将ax的值复制到es段寄存器中,es为附加段寄存器。
    • mov cx,#256:将256(十进制)复制到ax寄存器中。
    • sub si,si:即si = si - si,si置0。
    • sub di,di:即di = di - 地,di置0。
    • rep movw:rep表示重复执行后面的指令,movw 表示复制一个字(16位),重复执行256次(cx寄存器中的值),从ds:si处复制到es:di。
  3. 跳转到此处往后偏移go这个标签所代表的偏移地址处

    • jmpi go,INITSEG:跳转到0x90000 + go这个内存地址处执行,即跳转到mov ax,cs命令。
  4. 初步内存规划:将数据段寄存器 ds 和代码段寄存器 cs 设置为了 0x9000,方便代码的跳转与数据的访问。并将栈顶地址 ss:sp 设置在了离代码的位置 0x90000 足够遥远的 0x9FF00,保证栈向下发展不会轻易撞见代码的位置。复制值, ds,es和ss寄存器被赋值为0x9000。

    • mov ax,cs:将cs的值复制到ax寄存器中。cs寄存器表示代码段寄存器,由于段间跳转指令:jmpi go,INITSEG,cs寄存器里的值就是0x9000,ip寄存器里的值是go标签的偏移地址。cs:ip这组寄存器配合指向的是CPU当前正在执行的代码在内存中的位置,其中cs是基址,ip是偏移地址。
    • mov ds,ax:将ax的值复制到ds寄存器中。
    • mov es,ax:将ax的值复制到es寄存器中。
    • mov ss,ax:将ax的值复制到ss寄存器中,ss寄存器存放栈的段地址。
    • mov sp,#0xFF00:将0xFF00复制到sp寄存器中,SP(堆栈寄存器,stack pointer)存放栈的偏移地址。目前的栈顶地址就是ss:sp所指向的地址0x9FF00。
  5. 从硬盘的第2个扇区开始,把数据加载到内存0x90200处,共加载4个扇区。

    • mov dx,#0x0000:将0x0000复制到ax寄存器中。
    • mov cx,#0x0002:将0x0002复制到ax寄存器中。
    • mov bx,#0x0200:将0x0200复制到ax寄存器中。
    • mov ax,#0x0200 + SETUPLEN:将0x0200 + SETUPLEN复制到ax寄存器中。
    • int 0x13:int是汇编指令,int 0x13表示发起0x13号中断,dx,cx,bx,ax的值都将作为中断程序的参数。中断发起后,CPU会通过这个中断号,去寻找对应的中断处理程序的入口地址,并跳转过去执行,0x13 号中断的处理程序是BIOS提前给我们写好的,是读取磁盘的相关功能的函数。进入操作系统内核后,中断处理程序需要我们自己写。
    • jnc ok_load_setup:如果复制成功,就跳转到ok_load_setup这个标签,如果失败,则会不断重复执行这段代码。
  6. 将硬盘第6个扇区开始往后的240个扇区,加载到内存0x10000处。

    • call read_it:CALL,汇编代码,指令用于调用其他函数。
  7. 跳转执行

    • jmpi 0,SETUPSEG:跳转到内存中的0x90200处的代码,也就是硬盘中第二个扇区的代码。即setup.s文件的第一行代码。

setup.s文件:进入保护模式之前做的事

  1. 信息获取

    • 读取光标位置

      mov    ax,#INITSEG    ! this is done in bootsect already, but...
      mov    ds,ax
      mov    ah,#0x03    ! read cursor pos
      xor    bh,bh
      int    0x10        ! save it in known place, con_init fetches
      mov    [0],dx        ! it from 0x90000.
      • int 0x10:此中断触发BIOS提供的显示服务中断处理程序,而ah寄存器被赋值为0x03表示显示服务里具体的读取光标位置功能。int 0x10中断程序执行完毕并返回时,dx寄存器里的值表示光标的位置,其高八位dh存储了行号,低八位dl存储了列号(计算机在加电自检后会自动初始化到文字模式,在这种模式下,一屏幕可以显示 25 行,每行 80 个字符,也就是 80 列)。
      • mov [0],dx:把光标位置存储在[0]这个内存地址处。即0x9000处(加ds偏移地址)。
    • 获取内存信息,原理同读取光标位置

      mov    ah,#0x88
      int    0x15
      mov    [2],ax
    • 获取显卡显示模式,原理同读取光标位置

      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]
      mov ax,#INITSEG
      mov es,ax
      mov di,#0x0090
      mov cx,#0x10
      rep
      movsb
  2. 存储位置:最终存储在内存中的信息的位置

    内存地址长度(字节)名称
    0x900002光标位置
    0x900022扩展内存数
    0x900042显示页面
    0x900061显示模式
    0x900071字符列数
    0x900082未知
    0x9000A1显示内存
    0x9000B1显示状态
    0x9000C2显卡特性参数
    0x9000E1屏幕行数
    0x9000F1屏幕列数
    0x9008016硬盘1参数表
    0x9009016硬盘2参数表
    0x901FC2根设备号
  3. 关闭中断:我们之后要将BIOS写好的中断向量表给覆盖掉,写上我们自己的中断向量表,这个时不允许中断进入。

    cli            ! no interrupts allowed !
  4. 改变内存布局:把内存地址0x10000处开始往后一直到0x90000的内容,复制到内存的最开始的0位置;从0x90000地址开始记录着内存、硬盘、显卡等一些临时存放的数据;栈顶地址仍然是0x9FF00没有改变。

    mov    ax,#0x0000
    cld                 ! 'direction'=0, movs moves forward
    do_move:
    mov    es,ax        ! destination segment
    add    ax,#0x1000
    cmp    ax,#0x9000
    jz    end_move
    mov    ds,ax        ! source segment
    sub    di,di
    sub    si,si
    mov     cx,#0x8000
    rep
    movsw
    jmp    do_move
    • cmp ax,#0x9000:为第一个操作减去第二个操作数,但不影响第两个操作数的值,它影响flag的CF,ZF,OF,AF,PF,ZF=1 则说明两个数相等。
    • jz end_move:通过判断ZF标志位决定是否跳转,当执行到JZ或者JE指令时,如果ZF=1则跳转,如果ZF=0,不跳转。
    • rep movsw:内存地址0x10000处开始往后一直到0x90000的内容,复制到内存的最开始的0位置。这部分内容被system模块给占用,system 模块是除了bootsect和setup之外的全部程序链接在一起的结果,可以理解为操作系统的全部。
  5. 模式的转换:从16位的实模式转变为32位的保护模式。

    end_move:
    mov    ax,#SETUPSEG    ! right, forgot this at first. didn't work :-)
    mov    ds,ax
    lidt    idt_48         ! load idt with 0,0
    lgdt    gdt_48         ! load gdt with whatever appropriate
    ......
    gdt:
    .word    0,0,0,0       ! dummy
    
    .word    0x07FF        ! 8Mb - limit=2047 (2048*4096=8Mb)
    .word    0x0000        ! base address=0
    .word    0x9A00        ! code read/exec
    .word    0x00C0        ! granularity=4096, 386
    
    .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
    • lgdt gdt_48:加载全局描述符。在实模式下,ds寄存器里存储的值叫做段基址,在保护模式下叫段选择子,段选择子里存储着段描述符的索引,通过段描述符索引,可以从全局描述符表gdt中找到一个段描述符,段描述符里存储着段基址。计算机通过lgdt gdt_48指令加载全局描述符,把值(gdt_48)放在gdtr寄存器中。lgdt指令用于加载全局描述符表(gdt)寄存器,其操作数格式与lidt指令的相同。全局描述符表中的每个描述符项(8字节)描述了保护模式下数据和代码段(块)的信息。其中包括段的最大长度限制(16位)、段的线性基址(32位)、段的特权级、段是否在内存、读写许可以及其它一些保护模式运行的标志。
    • gdt_48:此处表示一个48位的数据,其中高32位存储着的正是全局描述符表gdt的内存地址,即0x90200 + gdt。
    • gdt:此处是全局描述符表在内存中的真正数据。目前全局描述符表有三个段描述符,第一个为空,第二个是代码段描述符(type=code),第三个是数据段描述符(type=data)
    • lidt idt_48:加载中断描述符表,原理同全局描述符表gdt。
  6. 打开 A20 地址线:突破地址信号线20位的宽度,变成32位可用。

    ! that was painless, now we enable A20
    call    empty_8042
    mov    al,#0xD1        ! command write
    out    #0x64,al
    call    empty_8042
    mov    al,#0xDF        ! A20 on
    out    #0x60,al
    call    empty_8042
    .......
    empty_8042:
    .word    0x00eb,0x00eb
    in    al,#0x64       ! 8042 status port
    test    al,#2        ! is input buffer full?
    jnz    empty_8042    ! yes - loop
    ret
    
  7. 对可编程中断控制器8259芯片进行编程(看不懂)。

    ! well, that went ok, I hope. Now we have to reprogram the interrupts :-(
    ! we put them right after the intel-reserved hardware interrupts, at
    ! int 0x20-0x2F. There they won't mess up anything. Sadly IBM really
    ! messed this up with the original PC, and they haven't been able to
    ! rectify it afterwards. Thus the bios puts interrupts at 0x08-0x0f,
    ! which is used for the internal hardware interrupts as well. We just
    ! have to reprogram the 8259's, and it isn't fun.
    
     mov    al,#0x11        ! initialization sequence
     out    #0x20,al        ! send it to 8259A-1
     .word    0x00eb,0x00eb        ! jmp $+2, jmp $+2
     out    #0xA0,al        ! and to 8259A-2
     .word    0x00eb,0x00eb
     mov    al,#0x20        ! start of hardware int's (0x20)
     out    #0x21,al
     .word    0x00eb,0x00eb
     mov    al,#0x28        ! start of hardware int's 2 (0x28)
     out    #0xA1,al
     .word    0x00eb,0x00eb
     mov    al,#0x04        ! 8259-1 is master
     out    #0x21,al
     .word    0x00eb,0x00eb
     mov    al,#0x02        ! 8259-2 is slave
     out    #0xA1,al
     .word    0x00eb,0x00eb
     mov    al,#0x01        ! 8086 mode for both
     out    #0x21,al
     .word    0x00eb,0x00eb
     out    #0xA1,al
     .word    0x00eb,0x00eb
     mov    al,#0xFF        ! mask off all interrupts for now
     out    #0x21,al
     .word    0x00eb,0x00eb
     out    #0xA1,al
    
    ! well, that certainly wasn't fun :-(. Hopefully it works, and we don't
    ! need no steenking BIOS anyway (except for the initial loading :-).
    ! The BIOS-routine wants lots of unnecessary data, and it's less
    ! "interesting" anyway. This is how REAL programmers do it.
    !

    经过重新编程后的芯片的引脚与中断号的对应关系:

    PIC 请求号中断号用途
    IRQ00x20时钟中断
    IRQ10x21键盘中断
    IRQ20x22接连从芯片
    IRQ30x23串口2
    IRQ40x24串口1
    IRQ50x25并口2
    IRQ60x26软盘驱动器
    IRQ70x27并口1
    IRQ80x28实时钟中断
    IRQ90x29保留
    IRQ100x2a保留
    IRQ110x2b保留
    IRQ120x2c鼠标中断
    IRQ130x2d数学协处理器
    IRQ140x2e硬盘中断
    IRQ150x2f保留
  8. 切换模式:从实模式切换到保护模式,将cr0这个寄存器的位0置1,就从实模式切换到保护模式了。

    ! Well, now's the time to actually move into protected mode. To make
    ! things as simple as possible, we do no register set-up or anything,
    ! we let the gnu-compiled 32-bit programs do that. We just jump to
    ! absolute address 0x00000, in 32-bit protected mode.
     mov    ax,#0x0001    ! protected mode (PE) bit
     lmsw    ax        ! This is it!
  9. 跳转到0x00执行:bin(8) = 0b00000000000001000,即描述符索引值是1。跳转到段基址是0,偏移也是0的地方执行,即内存地址的0地址处。零地址处以head.s文件开始。

    jmpi    0,8        ! jmp offset 0 of segment 8 (cs)
  10. 现在内存是什么模样。

    内存地址
    数据段描述符
    代码段描述符
    setup.s代码0x90200
    系统参数0x90000
    ......0x80000
    库模块
    内存管理模块
    内核模块
    main.c
    head.s0x00000

head.s文件:保护模式之后又该干什么

从这里开始,内核完全都是在保护模式下运行了。heads.s汇编程序与前面的语法格式不同,它采用的是AT&T的汇编语言格式,并且需要使用GNU的gas和gld2进行编译连接且代码中赋值的方向是从左到右。这段程序实际上处于内存绝对地址0处开始的地方。这个程序的功能比较单一:

  • 首先是加载各个数据段寄存器.

    pg_dir:
    .globl startup_32
    startup_32:
    movl $0x10,%eax
    mov %ax,%ds
    mov %ax,%es
    mov %ax,%fs
    mov %ax,%gs
    lss stack_start,%esp
    • _pg_dir:表示页目录,之后在设置分页机制时,页目录会存放在这里,也会覆盖这里的代码。
    • 连续五个mov操作:对于GNU汇编来说,每个直接数要以$开始,否则是表示地址。这里已经处于32位运行模式,因此这里的$0x10并不是把地址0x10装入各个段寄存器,它现在其实是全局段描述符表中的偏移值,或者更正确地说是一个描述符表项的选择符。这里分别给 ds,es,fs,gs 这几个段寄存器赋值为0x10,根据段描述符结构解析,表示这几个段寄存器的值为指向全局描述符表中的第二个段描述符,也就是数据段描述符。
    • lss指令让ss:esp栈顶指针指向了_stack_start标号的位置。原来的栈顶指针在0x9FF00。
  • 重新设置中断描述符表idt, 共256项,并使各个表项均指向一个只报错误的哑中断程序:中断描述符表 idt里面存储着一个个中断描述符,每一个中断号就对应着一个中断描述符,而中断描述符里面存储着主要是中断程序的地址,这样一个中断号过来后,CPU就会自动寻找相应的中断程序,然后去执行它。

    call setup_idt
    ......
    setup_idt:
    lea ignore_int,%edx
    movl $0x00080000,%eax
    movw %dx,%ax        /* selector = 0x0008 = cs */
    movw $0x8E00,%dx    /* interrupt gate - dpl=0, present */
    lea idt,%edi
    mov $256,%ecx
    rp_sidt:
    movl %eax,(%edi)
    movl %edx,4(%edi)
    addl $8,%edi
    dec %ecx
    jne rp_sidt
    lidt idt_descr
    ret
    ......
    ignore_int:
    pushl %eax
    pushl %ecx
    pushl %edx
    push %ds
    push %es
    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
    ......
    idt_descr:
    .word 256*8-1        # idt contains 256 entries
    .long idt
  • 然后重新设置全局描述符表gdt:因为原来设置的gdt是在setup程序中,之后这个内存区域要被缓冲区覆盖掉,所以重新设置在head程序中,这块内存区域之后不会被其他程序用到并覆盖。

    setup_gdt:
    lgdt gdt_descr
    ret
    • 和之前设置好的gdt一模一样。也是有代码段描述符和数据段描述符,然后第四项系统段描述符并没有用到。最后还留了 252 项的空间,这些空间后面会用来放置任务状态段描述符TSS和局部描述符LDT。
  • 接着使用物理地址0与1M开始处的内容相比较的方法,检测A20地址线是否已真的开启(如果没有开启,则在访问高于1Mb物理内存地址时CPU实际只会访问(IP MOD 1Mb)地址处的内容),如果检测下来发现没有开启,则进入死循环。

    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
    mov %ax,%gs
    lss stack_start,%esp
    xorl %eax,%eax
        incl %eax        # check that A20 really IS enabled
    movl %eax,0x000000    # loop forever if it isn't
    cmpl %eax,0x100000
    je 1b
  • 然后程序测试PC机是否含有数学协处理器芯片(80287、 80387 或其兼容芯片),并在控制寄存器CR0中设置相应的标志位。

    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
  • 接着设置管理内存的分页处理机制,将页目录表放在绝对物理地址0开始处(也是本程序所处的物理内存位置,因此这段程序将被覆盖掉),紧随后面放置共可寻址16MB内存的4个页表,并分别设置它们的表项。

    jmp after_page_tables
    ......
    after_page_tables:
    pushl $0        # These are the parameters to main :-)
    pushl $0
    pushl $0
    pushl $L6        # return address for main, if it decides to.
    pushl $main
    jmp setup_paging
    ......
    .org 0x1000 pg0:
    .org 0x2000 pg1:
    .org 0x3000 pg2:
    .org 0x4000 pg3:
    .org 0x5000
    ......
    setup_paging:
    movl $1024*5,%ecx         /* 5 pages - pg_dir+4 page tables */
    xorl %eax,%eax
    xorl %edi,%edi            /* pg_dir is at 0x000 */
    cld;rep;stosl
    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     /*  --------- " " --------- */
    movl $pg3+4092,%edi
    movl $0xfff007,%eax        /*  16Mb - 4096 + 7 (r/w user,p) */
    std
    stosl                      /* fill pages backwards - more efficient :-) */
    subl $0x1000,%eax
    jge 1b
    xorl %eax,%eax          /* pg_dir is at 0x0000 */
    movl %eax,%cr3          /* cr3 - page directory start */
    movl %cr0,%eax
    orl $0x80000000,%eax
    movl %eax,%cr0        /* set paging (PG) bit */
    ret                   /* this also flushes prefetch-queue */
    • 在没有开启分页机制时,由程序员给出的逻辑地址,需要先通过分段机制转换成物理地址。但在开启分页机制后,逻辑地址仍然要先通过分段机制进行转换,只不过转换后不再是最终的物理地址,而是线性地址,然后再通过一次分页机制转换,得到最终的物理地址。
    • CPU 在看到我们给出的内存地址后,首先把线性地址被拆分成:高 10 位:中间 10 位:后 12 位,高 10 位负责在页目录表中找到一个页目录项,这个页目录项的值加上中间 10 位拼接后的地址去页表中去寻找一个页表项,这个页表项的值,再加上后 12 位偏移地址,就是最终的物理地址。
    • 这一切的操作,都由计算机硬件MMU(内存管理单元)来负责将虚拟地址转换为物理地址。
    • 开启分页机制,更改cr0寄存器中的第31位。
    • linux-0.11认为,总共可以使用的内存不会超过16M,也即最大地址空间为0xFFFFFF。而按照当前的页目录表和页表这种机制,1个页目录表最多包含1024个页目录项(也就是1024个页表),1 个页表最多包含 1024 个页表项(也就是1024个页),1 页为 4KB(因为有 12 位偏移地址),因此,16M的地址空间可以用1个页目录表 + 4个页表搞定。
    • xorl %eax,%eaxmovl %eax,%cr3:告诉 cr3 寄存器,0 地址处就是页目录表,再通过页目录表可以找到所有的页表。
  • 最后利用返回指令将预先放置在堆栈中的init/main.c程序的入口地址弹出,去运行main程序。

    after_page_tables:
    pushl $0        # These are the parameters to main :-)
    pushl $0
    pushl $0
    pushl $L6        # return address for main, if it decides to.
    pushl $main
    jmp setup_paging
    L6:
    jmp L6            # main should never return here, but
                  # just in case, we know what happens.
    
    • setup_paging最后一个指令是 ret,它叫返回指令,CPU会很机械地把栈顶的元素值当做返回地址,跳转去那里执行。
    • 把esp寄存器(栈顶地址)所指向的内存处的值,赋值给eip寄存器,而cs:eip就是CPU要执行的下一条指令的地址。而此时栈顶刚好是main.c里写的main函数的内存地址,是我们特意压入栈的,所以CPU就理所应当跳过来了。

总结

  1. 三个汇编文件的总流程

    谁在处理流程干了啥
    开机
    BIOS加载启动区
    bootsect.s加载setup.s
    bootsect.s加载内核
    setup.s分段全局描述符GDT
    setup.s进入保护模式
    head.s中断机制中断描述符表IDT
    head.s分页机制页目录与页表
    head.s跳转到内核main.c
  2. 内存布局

    数据项地址
    栈顶sched.c文件中
    setup.s0x90200
    临时存放的变量0x90000
    ......0x80000
    操作系统全部代码
    页表3
    页表2
    页表1
    页表0
    页目录0x00000

声明:Hello World|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - linux0.11源码 - 启动项


我的朋友,理论是灰色的,而生活之树是常青的!