美文网首页
Linux0.11内核源码分析1-main函数运行之前的准备

Linux0.11内核源码分析1-main函数运行之前的准备

作者: CODERLIHAO | 来源:发表于2020-12-25 11:24 被阅读0次

    在阅读该文章之前,你起码有点操作系统的知识,了解实模式与保护模式的概念,了解分段机制,如果不懂得建议去阅读《操纵系统真象还原》这本书😁

    如果你不想看下面的分析,那么我就用这段话来简单描述启动main函数之前都需要做什么:

    init/main.c中得main函数启动前,我们需要加载内核,划分内存,启用分页,把实模式转变为保护模式等一系列操作。先加载bootsect,利用bootsect中得代码读取磁盘加载setup和system,然后跳转到setup运行,setup获取硬盘等关键数据,关闭中断,开启保护模式,setup没有开启保护模式前,一直都是实模式,段寄存器中得值也都是段地址,然后跳转到system运行,head就在system内存开始的位置,运行system就要先运行head,head就要为main准备好分页等操作准备,等这些做完后,main函数就开始运行了。

    如果你感兴趣,就继续往下看,坚持住~😄

    加电后,BIOS准备好中断向量表以及中断服务程序,在加载bootsect前,目前内存情情况


    image

    加载bootsect

    CPU复位后CS:IP = 0xF000:0xFFF0,这样CPU开始执行的第一条指令就在0xFFFF0这个物理地址。这个地址就在BIOS的范围,此时DRAM中还没有操作系统的代码。我们理解的内存条中的地址只占内存地址中的一部分,还有一部分需要给bios等其他硬件,所以你在电脑上看到的内存就是没有你算出来的大。

    接下来CPU会收到一个19号的中断,找到中断服务程序后就会把硬盘的0盘面0磁道1扇区的内容加载到0x07C00,然后跳转到该地址执行代码,此时DRAM中有了引导程序,为什么叫引导程序,因为需要它加载操作系统的内核文件。

    开始执行bootsect.s中的代码,起点就是entry _start

    boot/bootsect.s

    SETUPLEN = 4                ! 加载setup时需要的扇区数量
    BOOTSEG  = 0x07c0           ! bootsect被加载的地址
    INITSEG  = 0x9000           ! bootsect被复制到的新地址
    SETUPSEG = 0x9020           ! setup 程序执行的地址
    SYSSEG   = 0x1000           ! system 模块被加载的地址
    ENDSEG   = SYSSEG + SYSSIZE     ! system 模块内存中的末尾地址
    
    ROOT_DEV = 0x306
    
    entry _start
    _start:
        mov ax,#BOOTSEG  !寄存器 ax = 0x07c0
        mov ds,ax        !寄存器 dx = 0x07c0
        mov ax,#INITSEG  !寄存器 ax = 0x9000
        mov es,ax        !寄存器 es = 0x9000
        mov cx,#256      !寄存器 cx = 256,cx一般控制循环次数,因为后面执行的时rep指令和movw指令,所以这里的256是字,也就是512字节,也就是一个扇区
        sub si,si        !寄存器si中的值减去si后再放到si寄存器中,自己减去自己,就是si清零
        sub di,di        !寄存器di中的值减去di后再放到di寄存器中,di清零
        rep                  ! 重复执行一条指令,也就是后面的movw,一直到cx==0,这里循环了256次
        movw                 !把ds:si指向的字复制到es:di所指向的位置
        jmpi    go,INITSEG   !成功将bootsect在0x07c00的数据复制到0x90000物理地址,cs:ip =0x9000:go,go这是偏移地址,复制成功后就会跳转到该地址继续执行。
    go: mov ax,cs       !此时cs就是0x9000,所以寄存器ax的值就会是0x9000
        mov ds,ax       !寄存器 dx = 0x9000
        mov es,ax       !寄存器 es = 0x9000
    ! put stack at 0x9ff00.
        mov ss,ax       !寄存器 ss = 0x9000,ss与sp组合就表示栈
        mov sp,#0xFF00  !现在栈就在 ss:sp =  0x9000:0xFF00 = 0x9FF00
    

    上面的程序就是把bootsect的数据复制到0x9000这个物理地址,并在地址 0x9FF00准备了一个数据栈。


    image image
    ROOT_DEV = 0x306
    在bootsect.s中设置的根文件系统设备号其实只是初始值,不起作用,仅仅为保存根文件系统设备号的值在bootsect.s的编译后文件的508,509处预留了空间。而在最后用工具程序Build将所有内核有效部分组合起来时,还要对根文件系统设备号进行最后的处理。

    Makefile中定义了ROOT_DEV= #FLOPPY
    因为Linux当初是在Minix1.5.10操作系统的扩展版本Minix-i386上交叉编译开发的。并且当初Linus是将Linux的原始根文件系统放在第2块硬盘的第一个分区上的,所以在主Makefile文件中ROOT_DEV = /dev/hd6。并且在bootsect.s中,给定ROOT_DEV = 0x306;在Build.c中给定的缺省的主设备号(DEFAULT_MAJOR_ROOT)为3,缺省的次设备号(DEFAULT_MINOR_ROOT)为6。

    0x306指定根文件系统设备是第2个硬盘的第1个分区。这是Linux老式的硬盘命名方式,具体值的含义如下:

    设备号=主设备号*256 +次设备号(也即dev_no = (major<<8) + minor),其中主设备号:

    1-内存,2-磁盘,3-硬盘,4-ttyx,5-tty,6-并行口,7-非命名管道。

    硬盘的逻辑设备号(主设备号为3)

    逻辑设备号 对应设备文件 说明
    0x300 /dev/hd0 代表整个第1个硬盘
    0x301 /dev/hd1 表示第1个硬盘的第1个分区
    0x302 /dev/hd2 表示第1个硬盘的第2个分区
    0x303 /dev/hd3 表示第1个硬盘的第3个分区
    0x304 /dev/hd4 表示第1个硬盘的第4个分区
    0x305 /dev/hd5 代表整个第2个硬盘
    0x306 /dev/hd6 表示第2个硬盘的第1个分区
    0x307 /dev/hd7 表示第2个硬盘的第2个分区
    0x308 /dev/hd8 表示第2个硬盘的第3个分区
    0x309 /dev/hd9 表示第2个硬盘的第4个分区

    软盘的逻辑设备号:

    (主设备号为2,次设备号=type * 4 + nr,其中nr为0-3分别对应软驱A、B、C、D;type是软驱的类型2:1.2MB,7:1.44MB等)

    逻辑设备号 对应设备文件 说明
    0x021C /dev/PS0 1.44MB A驱动器,major = 2; minor = 7 * 4 + 0 = 28
    0x0208 /dev/at0 1.2MB A驱动器,major = 2;minor = 2 * 4 + 0 = 8

    bootsect.s文件被编译以后,产生的bin文件为512字节,其中508,509字节保存的为根设备号

    .org 508
    root_dev:
        .word ROOT_DEV
    

    bootsect被加载到0x90000处,所以从0x90000 + 508 = 0x901FC处即可获得根文件系统设备号的值。
    这个值在后面讲到的mian.c中有用到。

    加载setup

    load_setup:
        mov dx,#0x0000      ! drive 0, head 0
        mov cx,#0x0002      ! sector 2, track 0
        mov bx,#0x0200      ! 读取的扇区数据放在es:bx里面,es=0x9000,bx=512,紧贴在bootsect后面
        mov ax,#0x0200+SETUPLEN ! ah = 2表示读取扇区,al= 4表示需要读取的扇区数量
        int 0x13            !13号中断,ah里表示功能
        jnc ok_load_setup       ! ok - continue
        mov dx,#0x0000 
        mov ax,#0x0000      ! reset the diskette
        int 0x13            !ah =0 表示重置磁盘
        j   load_setup      !再次加载
    

    这里用到13号中断,关于13号中断的信息wiki 或者int_13,上面代码就是把0盘面0磁道2扇区的数据加载到bootsect的后面。

    ok_load_setup:
    
    ! 获取磁盘参数,尤其是每个磁道的扇区数
    
        mov dl,#0x00
        mov ax,#0x0800     ! AH=8 is get drive parameters
        int 0x13           ! 获取磁盘参数  
        mov ch,#0x00       ! ch清0,不要柱面数
        seg cs
        mov sectors,cx     !保存cx的数据到cs:[sectors]位置,ch=0,此时的cx 就是每个磁道的扇区数
        mov ax,#INITSEG
        mov es,ax          !把es设置为 0x9000
    
    seg cs
    mov sectors,cx
    

    就如同

    mov cs:[sectors],cx
    

    上面的代码把磁盘的扇区数柱面数ch和每个磁道的扇区数cl保存在cx中,由于ch=0,所以cx就表示每个磁道的扇区数。


    image

    在屏幕上显示Loading system ...

    ! Print some inane message
    
        
        mov ah,#0x03   ! int 0x10中断 功能号ah=0x03,读取光标位置
        xor bh,bh      ! bh 置为0,作为int 0x10中断的输入:bh=页号
        int 0x10       ! 发出中断, 返回:ch=扫描开始线;cl=扫描结束线;dh=行号; dl=列号
        mov cx,#24     ! 显示24个字符
        mov bx,#0x0007 ! bh=0,页=0;bl=7,字符属性=7
        mov bp,#msg1   ! es:bp寄存器指向要显示的字符串
    
        ! BIOS中断0x10功能号ah=0x13,功能:显示字符串
        ! 输入:al=放置光标方式及规定属性。0x01表示使用bl中属性值,光标停在字符串结尾处;
        ! es:bp 指向要显示的字符串起始位置。 cx=显示字符串个数; bh=显示页面号
        ! bl=字符属性; dh=行号; dl=页号
        mov ax,#0x1301      ! write string, move cursor
        int 0x10
        
            ........
       
       msg1:
        
        ! \r\n
        .byte 13,10
    
        ! ascii码"Loading system ..."占据18字节 
        .ascii "Loading system ..."
    
        ! \r\n\r\n
        .byte 13,10,13,10 
        
    

    加载system

        mov ax,#SYSSEG     ! ax = 0x1000
        mov es,ax          ! es = 0x1000
        call    read_it        !下一条指令call   kill_motor 的IP压栈,等待ret指令返回后弹出地址给IP
        call    kill_motor
        
           .....
           
    read_it:
        mov ax,es        ! ax = 0x1000
        test ax,#0x0fff  ! 这里确保ax是在64KB的边界,因为后面加载数据都是用64KB计算,正常情况下:test ax,#0x0fff结果为0,则ZF=1。不满足JNE跳转条件(ZF=0)
    die:    jne die      ! es must be at 64kB boundary
        xor bx,bx        ! 清bx 寄存器,用于表示当前段内存放数据的开始位置,为读取磁盘数据做好准备
    
    rp_read:
        mov ax,es       ! ax = 0x1000
    
        
        cmp ax,#ENDSEG      ! ax - ENDSEG的结果会修改ZF标志,如果结果为0,则ZF=1,否则ZF=0 用此判断有没有加载完毕
        
        ! jb指令当进位CF标志位为1时跳转到ok1_read标号处
        ! cmp是减法,如果CF = 1,说明有借位,此时ax的值比#ENDSEG的值小
        ! 说明没有读完,跳转ok1_read处执行
        jb ok1_read
    
        ret  ! 返回到call kill_motor出执行
    

    因为这里是段间转移,所以call指令就会把程序下一条指令的位置的IP压入堆栈中,然后转移到调用的子程序,ret就会把堆栈中的数据弹出给IP。

    ! 定义局部变量,已读扇区数,由于前面加载了bootsect(1个扇区数据)和setup(4个扇区数据),
    ! 所以这里1+SETUPLEN
    sread:  .word 1+SETUPLEN
    
    !磁头号
    head:   .word 0         ! current head
    
    ! 磁道号
    track:  .word 0         ! current track
    
    ok1_read:
        seg cs
        mov ax,sectors ! 把cs:[sectors]的值赋给ax
        sub ax,sread   ! ax = ax - sread , ax 表示磁道剩余扇区数
        mov cx,ax      ! cx 扇区数
        shl cx,#9      ! 乘以512,cx表示剩余字节数
        add cx,bx      ! bx已经被初始化为0,因为还是实模式,段寄存器只有16位,最大到64KB,这里cx表示读取了多少字节数
        
        jnc ok2_read   ! 若没有超过64KB 字节,则跳转至ok2_read 处执行
        je ok2_read    
        
            xor ax,ax      ! 超出最大段64KB,将ax清0
        sub ax,bx      ! 由于寄存器是16位无符号的,所以0 - bx = 65536 - bx,結果为段內剩余字节数
        shr ax,#9      ! ax>>9 转换成扇区数
    ok2_read:
        call read_track  ! 注意ax中的al存着我们需要读取的扇区数
        mov cx,ax        ! 读取后,al中保存着实际读取的扇区数数量,cx就表示这次已经读了多少扇区 
        add ax,sread     ! 该磁道上已经读取的扇区总数
        seg cs
        cmp ax,sectors   ! 如果当前磁道上的还有扇区未读,则跳转到ok3_read 处
        jne ok3_read
        mov ax,#1        ! 读该磁道的下一磁头面(1 号磁头)上的数据。如果已经完成,则去读下一磁道
        sub ax,head      ! 判断当前磁头号
        jne ok4_read     ! 如果是0 磁头,则再去读1 磁头面上的扇区数据
        inc track        ! 否则去读下一磁道
    ok4_read:
        mov head,ax      ! 保存当前磁头号
        xor ax,ax        ! 清0当前磁道已读扇区
    ok3_read:
        mov sread,ax     ! 保存当前磁道已读扇区数
        shl cx,#9
        add bx,cx
        jnc rp_read      ! 若小于64KB 边界值,则跳转到rp_read处,继续读数据
        mov ax,es        ! 否则调整当前段,为读下一段数据作准备
        add ax,#0x1000   ! 将段基址调整为指向下一个64KB 内存开始处
        mov es,ax
        xor bx,bx        ! 清段内数据开始偏移值
        jmp rp_read      ! 跳转至rp_read 继续读取
        
    read_track:
        push ax        ! 压栈保存数据
        push bx
        push cx
        push dx
        mov dx,track   ! dx存储磁道号
        mov cx,sread   ! cx表示已读扇区数(扇区号)
        inc cx         ! cx加1 ,表示读取下一个扇区
        mov ch,dl      ! dl=0,ch=0,表示0柱面
        mov dx,head   
        mov dh,dl      ! dh表示磁头号
        mov dl,#0      ! dl=0表示驱动号
        and dx,#0x0100 ! 保证磁头号不大于1
        mov ah,#2      ! ah表示功能号,2表示读取磁盘扇区,al中保存需要读取的扇区数量
        int 0x13       ! 发出中断,读取失败cf=1,ah会保存返回码,al保存实际读取扇区数,数据读取到es:bx指向的内存中
        jc bad_rt      ! jump carry,即cf=1时跳转,cf=1表示读取失败
        pop dx         ! 出栈
        pop cx
        pop bx
        pop ax
        ret
    bad_rt: mov ax,#0      ! 磁盘系统复位,跳转到read_track重新读取
        mov dx,#0
        int 0x13
        pop dx
        pop cx
        pop bx
        pop ax
        jmp read_track    
    

    INT 13h AH=02h的功能是读取扇区

    参数

    al:需要读取的扇区数
    ch:哪个柱面
    cl:哪个扇区
    dh:哪个磁头
    dl:哪个驱动
    

    输出ES:BX缓冲区,读取后,ah保存返回码,al保存实际读取扇区数量,如果读取失败CF置位1。

    !关闭软驱的马达
    
    kill_motor:
        push dx
        mov dx,#0x3f2
        mov al,#0
        outb
        pop dx
        ret
    

    获取root_dev跟设备号。,然后jmpi 0,SETUPSEG地址处执行,这个地方就是setup程序的地址。

            seg cs
        mov ax,root_dev     ! 定义了根设备号,就用该设备号,没有定义的跳转到root_defined
        cmp ax,#0
        jne root_defined
        seg cs
        mov bx,sectors
        mov ax,#0x0208      ! /dev/ps0 - 1.2Mb
        cmp bx,#15                  ! 如果sectors=15 则说明是1.2Mb 的驱动器;
        je  root_defined
        mov ax,#0x021c      ! /dev/PS0 - 1.44Mb
        cmp bx,#18                  ! 如果sectors=18 则说明是1.44Mb 的驱动器;
        je  root_defined
    undef_root:
        jmp undef_root
    root_defined:
        seg cs
        mov root_dev,ax
    
    ! after that (everyting loaded), we jump to
    ! the setup-routine loaded directly after
    ! the bootblock:
    
        jmpi    0,SETUPSEG
    
    image

    执行setup程序

    start:
        mov ax,#INITSEG ! ax =  0x9000
        mov ds,ax           ! ds =  0x9000
        mov ah,#0x03    ! read cursor pos
        xor bh,bh
        int 0x10        ! save it in known place, con_init fetches
        mov [0],dx      ! 把光标位置保存到ds:[0],也就是0x90000
    

    int 0x10中断在这里查看INT_10H

    BIOS 中断0x10 的读光标功能号 ah = 0x03

    输入:bh = 页号

    返回:ch = 扫描开始线,cl = 扫描结束线,
    dh = 行号(0x00 是顶端),dl = 列号(0x00 是左边)。

    ! Get memory size (extended mem, kB)
    
        mov ah,#0x88
        int 0x15
        mov [2],ax
    

    获取扩展内存的大小值(KB)。

    调用中断0x15,功能号ah = 0x88
    返回:ax = 从0x100000(1M)处开始的扩展内存大小(KB)。 若出错则CF 置位,ax = 出错码。

    ! Get video-card data:
    
        mov ah,#0x0f
        int 0x10
        mov [4],bx      ! bh = display page
        mov [6],ax      ! al = video mode, ah = window width
    

    获取显示卡当前显示模式。

    调用BIOS 中断0x10,功能号 ah = 0x0f

    返回:ah = 字符列数,al = 显示模式,bh = 当前显示页。
    0x90004(1 字)存放当前页,0x90006 显示模式,0x90007 字符列数。

    ! check for EGA/VGA and some config parameters
    
        mov ah,#0x12
        mov bl,#0x10
        int 0x10
        mov [8],ax
        mov [10],bx
        mov [12],cx 
    

    检查显示方式(EGA/VGA)并取参数

    调用BIOS 中断0x10

    功能号:ah = 0x12,bl = 0x10

    返回:

    bh = 显示状态 (0x00 - 彩色模式,I/O 端口=0x3dX) 、(0x01 - 单色模式,I/O 端口=0x3bX)

    bl = 安装的显示内存(0x00 - 64k, 0x01 - 128k, 0x02 - 192k, 0x03 = 256k)

    cx = 显示卡特性参数

    ! Get hd0 data
    
        mov ax,#0x0000
        mov ds,ax
        lds si,[4*0x41] ! 取中断向量0x41 的值,也即hd0 参数表的地址ds:si
        mov ax,#INITSEG
        mov es,ax
        mov di,#0x0080
        mov cx,#0x10
        rep
        movsb
    ! Get hd1 data
    
        mov ax,#0x0000
        mov ds,ax
        lds si,[4*0x46]  ! 取中断向量0x46 的值,也即hd1 参数表的地址
        mov ax,#INITSEG
        mov es,ax
        mov di,#0x0090
        mov cx,#0x10
        rep
        movsb
        
           ! 检查系统是否存在第2 个硬盘,如果不存在则第2 个表清零
        mov ax,#0x01500
        mov dl,#0x81
        int 0x13
        jc  no_disk1
        cmp ah,#3
        je  is_disk1
    no_disk1:
        mov ax,#INITSEG  ! 第2 个硬盘不存在,则对第2 个硬盘表清零
        mov es,ax
        mov di,#0x0090
        mov cx,#0x10
        mov ax,#0x00
        rep
        stosb
    
    

    取第一个硬盘的信息(复制硬盘参数表)
    第1个硬盘参数表的首地址竟然是中断向量0x41 的向量值!而第2 个硬盘参数表紧接第1 个表的后面,
    0x90080 处存放第1 个硬盘的表,0x90090 处存放第2 个硬盘的表。

    利用BIOS 中断调用0x13 的取盘类型功能。

    功能号 ah = 0x15;

    输入:dl = 驱动器号(0x8X 是硬盘:0x80 指第1 个硬盘,0x81 第2 个硬盘)

    输出:ah = 类型码;00 --没有这个盘,CF 置位; 01 --是软驱,没有change-line 支持;
    02 --是软驱(或其它可移动设备),有change-line 支持; 03 --是硬盘。

    is_disk1:
    
         ! 现在我们要进入保护模式中
    
       cli         ! 关闭中断
    
    ! first we move the system to it's rightful place
    
       mov ax,#0x0000
       cld         ! 'direction'=0, movs moves forward
    

    这里补充一点关于
    movsb、movsw的知识,因为后面的代码会用到相关知识,
    这两个指令通常用于把数据从内存中的一个地方批量地传送(复制)到另一个地方,处理器把它们看成数据串。但是,movsb的传送是以字节为单位的,而movsw的传送是以字为单位的。
    movsbmovsw指令执行时,原始数据串的段地址由DS指定,偏移地址由SI指定,简写DS:SI;要传送到的目的地址由ES:DI指定;传送的字节数(movsb)或者字数(movsw)由CX指定。除此之外,还要指定是正向传送还是反向传送,正向传送是指传送操作的方向是从内存区域的低地址端到高地址端;反向传送则正好相反。正向传送时,每传送一个字节(movsb)或者一个字(movsw),SIDI加1或者加2;反向传送时,每传送一个字节(movsb)或者一个字(movsw)时,SIDI减去1或者减去2。不管是正向传送还是反向传送,也不管每次传送的是字节还是字,每传送一次,CX的内容自动减1。
    标志寄存器的第10位是方向标志DF(Direction Flag)DF=0表示正向传送,DF=1表示反向传送。
    cld指令将DF标志清零,std指令将DF标志置1

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

    do_move:
        mov es,ax       ! 复制到的目的地址es:di = 0x0000:0x0
        add ax,#0x1000
        cmp ax,#0x9000       ! 已经把从0x8000 段开始的64k 代码移动完?
        jz  end_move
        mov ds,ax       ! 复制的源地址ds:si = 0x1000:0x0
        sub di,di
        sub si,si
        mov     cx,#0x8000       ! 移动0x8000 字(64k 字节)
        rep
        movsw
        jmp do_move
    
    end_move:
        mov ax,#SETUPSEG    ! right, forgot this at first. didn't work :-)
        mov ds,ax           ! ds 指向本程序(setup)段
        lidt    idt_48      ! 加载中断描述符到idtr寄存器
        lgdt    gdt_48      ! 加载全局描述符到gdtr寄存器
    
    ! 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
        
        ......
        
            mov ax,#0x0001  ! 启用保护模式
        lmsw    ax      ! This is it!
        jmpi    0,8     ! 现在处于保护模式了,段寄存器里面就不是段地址了,而是段选择符了
        
    

    我们已经将system 模块移动到0x00000 开始的地方,所以这里的偏移地址是0,这里的段值的8 已经是保护模式下的段选择符了,用于选择描述符表和描述符表项以及所要求的特权级。所以段选择符
    8(0b0000,0000,0000,1000)表示请求特权级0、使用全局描述符表中的第1 项,该项指出代码的基地址是0
    因此这里的跳转指令就会去执行system 中的代码。

    gdt:                            !全局描述符表开始处。描述符表由多个8 字节长的描述符项组成
        .word   0,0,0,0     ! 第1 个描述符,不用
            !代码段描述符  
        .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       ! 全局表长度为2k 字节,因为每8 字节组成一个段描述符项,所以表中共可有256 项
        .word   512+gdt,0x9 ! 4 个字节构成的内存线性地址:0x0009<<16 + 0x0200+gdt,也即0x90200 + gdt(即在本程序段中的偏移地址)
        
    
    
    image

    图片来源英特尔® 64 位和 IA-32 架构开发人员手册:卷 3A 第196页

    image 段选择符

    head.s开始执行

    因为head.s是位于system开始,所以,jmpi 0,8就是执行head.s代码,注意这里不是intel汇编格式,而是AT&T汇编。

    startup_32:
        movl $0x10,%eax  # eax= 10
        mov %ax,%ds
        mov %ax,%es
        mov %ax,%fs
        mov %ax,%gs
        lss _stack_start,%esp  # ss:esp 设置堆栈,stack_start在sched.c中
        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
        mov %ax,%gs
        lss stack_start,%esp
        xorl %eax,%eax
    1:  incl %eax       # check that A20 really IS enabled
        movl %eax,0x000000  # loop forever if it isn't
        cmpl %eax,0x100000
        je 1b                   # '1b'表示向后(backward)跳转到标号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
    

    这里已经处于32 位运行模式,因此这里的0x10 就是段选择符,这里0x10 的含义是请求特权级0(位0-1=0)、选择全局描述符表(位2=0)、选择表中第2 项(位3-15=2)。它正好指向GDT表中的数据段描述符项

    [图片上传失败...(image-6c7038-1608866635403)]

    setup_idt:
        lea ignore_int,%edx   # 将ignore_int 的有效地址(偏移值)保存到edx 寄存器
        movl $0x00080000,%eax # 将选择符0x0008 置入eax 的高16 位中
        movw %dx,%ax          # 偏移值的低16 位置入eax 的低16 位中
        movw $0x8E00,%dx      # interrupt gate - dpl=0, p=1 此时edx 含有门描述符高4 字节的值
    
        lea idt,%edi          # _idt 是中断描述符表的地址
        mov $256,%ecx         # 256次循环,
    rp_sidt:
        movl %eax,(%edi)     # 把32位寄存器eax的值赋给es:edi的内存位置
        movl %edx,4(%edi)    # 把32位寄存器eax的值赋给es:edi + 4的内存位置
        addl $8,%edi         # edi 指向表中下一项,描述符占据8字节
        dec %ecx
        jne rp_sidt          # 加载256个描述符
        lidt idt_descr       # 加载中断描述符表寄存器值
        ret
    setup_gdt:
        lgdt gdt_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
        popl %eax
        pop %fs
        pop %es
        pop %ds
        popl %edx
        popl %ecx
        popl %eax
        iret
        
    

    上面也讲过48位idtr寄存器得结构,.word表示一个字,也就是2个字节(16位),idrtd的低16位白表示的是limit,被限制多少字节,256*8-1中的乘以8是因为中断描述符也是占据8个字节,所以表示的就是256个中断描述符,现在idt地址处被清0了,等待main函数启动收重新设置中断。

    idt_descr

    idt_descr:
        .word 256*8-1       # idt contains 256 entries
        .long idt
    .align 2  # 2的2次方内存对其,也就是4字节对齐
    .word 0
    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  # 2的8次方内存对齐,64字节对齐
    idt:    .fill 256,8,0       # idt 清0,等待main函数启动后重新填值
    
    gdt:    .quad 0x0000000000000000    /* NULL描述符,要有,但是不用,防止哪个傻蛋用了这个 */
        .quad 0x00c09a0000000fff    /* 16Mb */
        .quad 0x00c0920000000fff    /* 16Mb */
        .quad 0x0000000000000000    /* TEMPORARY - don't use */
        .fill 252,8,0           /* space for LDT's and TSS's etc */
    
    
    after_page_tables:
        pushl $0        # These are the parameters to main :-)
        pushl $0
        pushl $0
        pushl $L6       # 如果main函数退出,就会到L6处死循环,main函数是内核运行的,一般不断电不会退出
        pushl $_main    # 把main函数的地址压栈,等待分页完成后就会执行
        jmp setup_paging
    L6:
        jmp L6
       
    

    开始分页,为什么分页以及分页的原理,建议大家可以看看《操作系统真象还原》这本书。
    普及一下概念,页表中的每一行(只有一个单元格)称为页表项(Page Table Entry,
    PTE),其大小是4 字节,页表项的作用是存储内存物理地址。

    由于页大小是4KB,所以页表项中的物理地址都是4K 的整数倍,故用十六进制表示的地址,低3 位都是0,比如0x1000,0x2000,0x3000,为什么我在这里说这个,因为后面分析PTE时会有用。

    以为寄存去是32位的了,内存寻址可以达到4GB,也就是说每个应用都可以访问4GB的内存,可是不止我们一个应用,还有其他的应用也要用内存,如果都要占用4GB,肯定会有冲突,分页后,应用按需加载,每个应用都会有4GB的虚拟内存,注意这里是虚拟内存,并不是真正的内存,想要PTE中有物理地址,经过MMU转换后就可以得到真实的物理地址了,现在1页是4KB,虚拟内存是4GB,所以可以划出来 4GB/4KB=1M 个页,也就是4GB 空间中可以容纳1048576 个页,页表中自然也要有1048576 个页表项。

    下面分析如何通过一级页表得到物理地址的:

    在 32 位保护模式下任何地址都是用32 位二进制表示的,包括虚拟地址也是。虚拟地址的高20 位可用来定位一个物理页,低12 位可用来在该物理页内寻址(偏移地址)。

    一个页表项对应一个页,所以,用线性地址的高20 位作为页表项的索引,每个页表项要占用4 字节
    大小,所以这高20 位的索引乘以4 后才是该页表项相对于页表物理地址的字节偏移量。用cr3 寄存器中
    的页表物理地址加上此偏移量便是该页表项的物理地址,从该页表项中得到映射的物理页地址,然后用线
    性地址的低12 位与该物理页地址相加,所得的地址之和便是最终要访问的物理地址。


    一级页表将线性地址转换成物理地址过程

    说一级页表就是为了说明原理。但我们这里用的是二级页表,下面分析二级页表
    每个页表都有自己的页表,这样就光页表就占据不少内存,一级页表在线代操作系统也不会用。
    一级页表是将这1M 个标准页放置到一张页表中,二级页表是将这1M 个标准页平均放置1K 个
    页表中。每个页表中包含有1K 个页表项。页表项是4 字节大小,页表包含1K 个页表项,故页表大小为
    4KB,为了管理二级页表,就要有页目录,每个页表的物
    理地址在页目录表中都以页目录项(Page Directory Entry, PDE)的形式存储,页目录项大小同页表项一
    样,都用来描述一个物理页的物理地址,其大小都是4 字节,而且最多有1024 个页表,所以页目录表也
    是4KB 大小,同样也是标准页的大小,也就是说页目录只占用1页。经过页目录,程序可以访问的物理地址为
    1024(1024个页目录) * 1024(每个PDE指向1024个PTE)*4KB(每个PTE指向4KB物理内存) = 4GB,虽然可以理论可以访问这么多地址,实际上不会的,就光页表还要占据内存,为了和操作系统交互,这4GB还要给内核分配一点占据高地址的1GB,自己的程序还剩下3GB了。呵呵呵~~~

    二级页表

    二级页表与一级页表在原理上相同,但结构上已经有了很大不同,它们在虚拟地址到物理地址转换方
    法上也有很大不同。

    一级页表转换方法,是将32 位虚拟地址拆分成两部分,高20 位用于定位一个物理页,低 12 位用于物理页内的偏移量。

    在二级页表是这样的:
    页目录中1024 个页表,只需要10 位二进制就能够表示了,所以,虚拟地址的高
    10 位(第31~22 位)用来在页目录中定位一个页表,也就是这高10 位用于定位页目录中的页目录项PDE,
    PDE 中有页表物理页地址。找到页表后,到底是页表中哪一个物理页呢?由于页表中可容纳1024 个物理页,
    故只需要10 位二进制就能够表示了。所以虚拟地址的中间10 位(第21~12 位)用来在页表中定位具体的
    物理页,也就是在页表中定位一个页表项PTE,PTE 中有分配的物理页地址。由于标准页都是4KB,12 位
    二进制便可以表达4KB 之内的任意地址,故线性地址中余下的12 位(第11~0 位)用于页内偏移量。
    无论是PDE还是PTE,地址中的都是索引,每个PDE或者PTE都占据4字节,所以会乘以4用来表示访问地址

    二级页表虚拟地址到物理地址转换

    下面看看PDE或者PTE数据结构


    image

    4 字节大小,但其内容并不全是物理地址,只有第12~31 位才是物理地址,这才20 位,因该是32位啊~,
    因为页目录项和页表项中的都是物理页地址,标准页大小是4KB,故地址都是4K 的倍数,也就是地址的低12
    位是0,所以只需要记录物理地址高20 位就可以啦。这样省出来的12 位。这里的每一项具体是什么含义,这里说不下,书中自有黄金屋~~~。感觉就分页都能讲一章了😂

    最后在说下控制寄存器cr3,它是用来保存页目录的

    image
    启动分页机制的开关是将控制寄存器cr0 的PG 位置1

    好了回到我们的linux分析,绕了一大圈回来了,有没有看懂分页,有没有懵逼~🤣

    setup_paging一共分了5页,1个页目录,4个页表,stosl 指令相当于将eax 中的值保存到ES:EDI 指向的地址中,若设置了标志寄存器EFLAGS中的方向位置位(即在stosl指令前使用STD指令)则EDI自减4,否则(使用CLD指令)EDI自增4。所以这里ecx的值就是10245,每次移动4个字节,10245就是移动了1024* 5 * 4个字节,也就是5KB(5页)。pg_dir的初始位置在0x0000这个位置。

    movl $pg0+7,pg_dir表示将pg0+7的值放到pg_dir的位置,此时pg_dir的位置在0x0000, $pg0+7的值是0x00001007,则第1 个页表所在的地址 = 0x00001007 & 0xfffff000 = 0x1000;第1 个页表的属性标志 = 0x00001007 & 0x00000fff = 0x07=0b0111,P=1,RW=1,US=1 表示该页存在、用户可读写。

    setup_paging:
        movl $1024*5,%ecx       /* 5 pages - pg_dir+4 page tables */
        xorl %eax,%eax                  /* eax 清0 */
        xorl %edi,%edi          /* edi 清0  pg_dir is at 0x000  */
        cld;rep;stosl                   /* cld将df置位0,表示正向传送 ,重复执行stosl */
           
           /* 初始化页目录 */
        movl $pg0+7,pg_dir      /*  第1个PDE,$pg0+7表示0x00001007 */
        movl $pg1+7,pg_dir+4        /*  第2个PDE,每个PTE占据4字节,所以加4 */
        movl $pg2+7,pg_dir+8        /*  第3个PDE,前面已经有2个PDE了,所以加8 */
        movl $pg3+7,pg_dir+12       /*  第4个PDE,前面已经有3个PDE了,所以加12 */
        
            movl $pg3+4092,%edi             /*一个页表的最后一项在页表中的位置是1023*4 = 4092。 因此最后一页的最后一项的位置就是$pg3+4092*/
        movl $0xfff007,%eax     /*  16Mb - 4096 + 7 (r/w user,p) */
        std                             /*  方向位置位,edi 值递减(4 字节) */
    1:  stosl           /* fill pages backwards - more efficient :-) */
        subl $0x1000,%eax
        jge 1b
        xorl %eax,%eax      /* 页目录表在0x0000 处 */
        movl %eax,%cr3      /* 设置页目录基址寄存器cr3 的值,指向页目录表 */
        movl %cr0,%eax
        orl $0x80000000,%eax   
        movl %eax,%cr0      /* 添上PG 标志,启用分页 */
        ret         /* this also flushes prefetch-queue */
    
    .org 0x1000
    pg0:
    
    .org 0x2000
    pg1:
    
    .org 0x3000
    pg2:
    
    .org 0x4000
    pg3:
    
    .org 0x5000
    

    这是head执行完后,内存分布,图片来源《Linux_内核完全注释_V11》


    image

    但是我觉的这么画可能更容易理解


    image

    相关文章

      网友评论

          本文标题:Linux0.11内核源码分析1-main函数运行之前的准备

          本文链接:https://www.haomeiwen.com/subject/qymbnktx.html