[026][x86汇编语言]第十三章 学习内核程序 c13_core.asm

学习笔记

《x86汇编语言:从实模式到保护模式》
https://www.jianshu.com/p/d481cb547e9f

第十三章的 代码

  • 用户程序 c13.asm 代码行数81行
  • 内核程序 c13_core.asm 代码行数601行
  • 加载程序 c13_mbr.asm 代码行数221行

加载程序 c13_mbr.asm

https://www.jianshu.com/p/49cbc4161799

用户程序 c13.asm

https://www.jianshu.com/p/8b56ee466735

内核程序部分源码(取自 c13_core.asm,增加注释)

1、子程序 allocate_memory

;-------------------------------------------------------------------------------
;子例程:allocate_memory
;-------------------------------------------------------------------------------
;输入:ECX=希望分配的字节数
;输出:ECX=起始线性地址(本次分配的起始地址)
;-------------------------------------------------------------------------------
allocate_memory:
        push ds
        push eax
        push ebx
        
        mov eax,core_data_seg_sel
        mov ds,eax
        
        mov eax,[ram_alloc]     ;标号ram_alloc此时存着本次分配的起始线性地址
        add eax,ecx             ;起始地址加上要分配的字节数形成下一次分配的起始地址
        
        mov ecx,[ram_alloc]     ;返回本次分配的起始地址
        
        ;强制起始地址是4字节对齐的
        mov ebx,eax             ;将eax的值备份到ebx
        
        and ebx,0xfffffffc      ;0xfffffffc=1111_1111_1111_1111_1111_1111_1111_1100B
        add ebx,4               ;0x4 = 0100B
        
        test eax,0x00000003     ;0x00000003=0000_0000_0000_0000_0000_0000_0000_0011B
        cmovnz eax,ebx          ;如果没有对齐则采用强制对齐后的数值ebx,否则保持原样
        
        mov [ram_alloc],eax     ;将可用于下一次分配的起始地址回写到标配ram_alloc
        
        pop ebx
        pop eax
        pop ds


retf    ;段间调用

;-------------------------------------------------------------------------------
  • ram_alloc dd 0x00100000 ;下次分配内存时的起始地址,具体的访问方式结合 内核数据段选择子core_data_seg_sel, 选择子:偏移地址ram_alloc里面存着的本质就是偏移地址;

  • allocate_memory 结合 标号 ram_alloc 处的双字0x0010 0000,这就是可用于分配的初始内存地址(之后整个用户程序以及分配给用户程序的栈都从这个地址开始放);

  • allocate_memory调用过程中,ram_alloc 标号后的数据每一次新的分配后,会被改写成新的起点地址,因此,本次分配的起始线性地址由ECX传回,下一次可用的起始线性地址被回写到标号ram_alloc

    [026][x86汇编语言]第十三章 学习内核程序 c13_core.asm_第1张图片
    内核程序 allocate_memory ram_alloc dd 0x0010000.png

  • test 是做and运算,但是不改变寄存器结果;

  • cmovnz eax,ebx,如果运算结果不为零(说明eax末两位不是00,即没有对齐),就用ebx的值覆盖eax的值;

2、从allocate_memory 返回的ECX 要结合段选择子 mem_0_4_gb_seg_sel (指向0~4GB) 使用

  • 第2行的 call sys_routine_seg_sel:allocate_memory 进行了调用,之后传回 ECX=本次分配内存的起始地址
;-------------------------------------------------------------------------------
; 以下代码位于 子程序 load_relocate_program
;-------------------------------------------------------------------------------
    mov ecx,eax                        ;实际需要申请的内存数量
    call sys_routine_seg_sel:allocate_memory
         
    mov ebx,ecx                        ;ebx -> 申请到的内存首地址  | 组成 【DS:EBX=目标缓冲区地址】
    push ebx                           ;保存该首地址 
      
    xor edx,edx                      ;edx高32位置为零
    mov ecx,512
    div ecx                      ;edx余数 eax商(即扇区个数)

    mov ecx,eax                        ;总扇区数 传递给 ecx | 组成【读扇区操作的循环次数】
      
    mov eax,mem_0_4_gb_seg_sel         ;切换DS到0-4GB的段
    mov ds,eax

        mov eax,esi                        ;起始扇区号 | 组成【EAX=逻辑扇区号】
  .b1:
         call sys_routine_seg_sel:read_hard_disk_0
         inc eax
         loop .b1  

;-------------------------------------------------------------------------------
;read_hard_disk_0:                           ;从硬盘读取一个逻辑扇区
;                                            ;EAX=逻辑扇区号
;                                            ;DS:EBX=目标缓冲区地址
;                                            ;返回:EBX=EBX+512
;-------------------------------------------------------------------------------
  • 选择子mem_0_4_gb_seg_sel索引号是0x08,是一个指向0~4GB全部内存空间的数据段,段基地址是0x0000 0000(全零),因此,结合0x0001 0000 ,组成段选择子:偏移地址真实物理地址 就是0x 0010 0000
  • 为什么这个段的段基地址是0x0000 0000

https://www.jianshu.com/p/49cbc4161799

3、子程序:make_seg_descriptor 构造存储器和系统的段描述符

;-------------------------------------------------------------------------------
;子程序:make_seg_descriptor
;-------------------------------------------------------------------------------
;功能:构造存储器和系统的段描述符
;输入:EAX=线性基地址
;      EBX=段界限
;      ECX=属性。各属性位都在原始位置,无关的位清零 
;返回:EDX:EAX=描述符
;-------------------------------------------------------------------------------                                            
make_seg_descriptor:
        
        mov edx,eax
        shl eax,16
        or ax,bx                ;描述符的前32位(EAX)构造完毕
        
        and edx,0xffff0000      ;清除基地址中无关的位
        rol edx,8               
        bswap edx               ;装配基地的31~24和23~16(80486+)
        
        xor bx,bx               
        or edx,ebx              ;装配段界限的高4位
        
        or edx,ecx              ;装配属性

retf

4、子程序:set_up_gdt_descriptor 在GDT内安装一个新的描述符

;-------------------------------------------------------------------------------
;子程序:set_up_gdt_descriptor
;-------------------------------------------------------------------------------
;功能:    在GDT内安装一个新的描述符
;输入:    EDX:EAX=描述符(64位描述符)
;输出: CX=描述符的选择子
;-------------------------------------------------------------------------------
set_up_gdt_descriptor:
        
        push eax
        push ebx
        push edx
        
        push ds
        push es
        
        mov ebx,core_data_seg_sel       ;切换到内核数据段
        mov ds,ebx
        
        ;--------------------------------------------------------
        ;pgdt            dw  0             ;用于设置和修改GDT  ;
        ;                dd  0                                  ;
        ;--------------------------------------------------------
        sgdt [pgdt]                     ;将GDTR寄存器的基地址和界限值保存到pdgt处
                                        ;GDT的段基地址自 加载程序 以来 一直是0x007E 0000
        ;--------------------------------------------------------
        ;pgdt            dw  N             ;用于设置和修改GDT  ;
        ;                dd  0x007E000                          ;
        ;--------------------------------------------------------                               
                                        
        mov ebx,mem_0_4_gb_seg_sel      ;指向0~4GB内存的段的选择子
        mov es,ebx
        
        movzx ebx,word [pgdt]           ;GDT界限
        inc bx                          ;
        add ebx,[pgdt+2]                ;下一个描述符的线性地址
        
        mov [es:ebx],eax                ;在GDT表中填入新的描述符 低32位
        mov [es:ebx+4],edx              ;在GDT表中填入新的描述符 高32位
        
        add word [pgdt],8               ;增加GDT界限值
        
        lgdt [pgdt]                     ;重新加载GDTR寄存器,使新的描述符生效
        
        mov ax,[pgdt]                   ;得到GDT的界限值
        xor dx,dx
        mov bx,8
        div bx
        mov cx,ax
        shl cx,3                        ;将索引号移到正确位置
        
        
        pop es
        pop ds
        
        pop edx
        pop ebx
        pop eax

retf
  • 联系一下 加载程序 c13_mbr.asm

https://www.jianshu.com/p/49cbc4161799

加载程序里有 2条 lgdt 指令 以修改GDTR寄存器的内容,使得新的描述符生效,分别是:
;--------------------------------------------------------------------------
;加载GDT表 偏移+00~+20 5个项目
;--------------------------------------------------------------------------
lgdt [cs:pdgt+0x7c00]

;--------------------------------------------------------------------------
;再加载GDT表 偏移+28~+38 3个项目
;--------------------------------------------------------------------------
lgdt [0x7c00+pdgt]

;--------------------------------------------------------------------------
;表
;--------------------------------------------------------------------------
    pdgt    dw 0
            dd 0x00007e00       ;GDT的物理地址
  • 再看本文的 内核程序 c13_core.asm 中相关语句,也是2条指令,分别是:
读取GDTR 寄存器内容的 sgdt 
 sgdt [pgdt]                     ;将GDTR寄存器的基地址和界限值保存到pdgt处
                                        ;GDT的段基地址自 加载程序 以来 一直是0x007E 0000
        ;--------------------------------------------------------
        ;pgdt            dw  N             ;用于设置和修改GDT  ;
        ;                dd  0x007E000                          ;
        ;--------------------------------------------------------     

>****************************************************************************************<
修改GDTR寄存器内容的 lgdt 
  lgdt [pgdt]                     ;重新加载GDTR寄存器,使新的描述符生效
  • 对比一下,就不能理解这条注释了;GDT的段基地址自 加载程序 以来 一直是0x007E 0000
因为 加载程序 在时间上 是比 内核程序 要先执行的,
在 加载程序 的执行过程中,两条lgdt 指令 仅仅只是在不断改变 GDT的段界限,
新增一个描述符或者新增一批描述符,不断增加的只是段界限的值,
而段的基地址一直都是 pgdt 标号里 后4个字节的 0x007E 0000

因此,我才在注释里写上了,自加载程序以来,GDT的段地址一直都是0x007E 0000;

现在,CPU转移到了内核程序开始执行,
内核程序 也开辟了一段6字节的内存空间,在标号pgdt之后,
在内核程序的指令里,我们是先用一条 sgdt 指令直接就读出了【GDTR寄存器】里面的内容,
随之放到了这个 pgdt 标号开辟的内存空间,
换言之,就是不要被 内核程序中 pgdt初始的全零数据给迷惑了;
这是 sgdt ,读的是 【GDTR 寄存器】里面的数据。
  • 想要强调的问题
写汇编程序,不可避免地要考虑,程序真正执行起来,动起来的状态,
那个时候,这些程序,加载程序也好、内核程序也好,用户程序也好,
它们不仅都有办法访问整个内存空间,
那么某一段特殊的内存空间就可以看做是高级语言里的“全局变量”,

比喻也许不恰当,但是我想表达的就是这些变量被传递、被很多程序共同使用;
同时,还要意识到有一个东西也是这样的, 传递数据、又被很多程序共同使用,
那就是CPU里面的寄存器,比如这里的GDTR寄存器。

>****************************************************************************************<
加载程序 设置 与自己相关的段的 GDT 描述符;
;   +20         文本模式显存(000B8000~00BFFFFF)       0x20
;   +18         初始栈段(00006C00~00007C00)         0x18
;   +10         初始代码段(00007C00~00007DFF)            0x10
;   +08         0~4GB数据段(00000000~FFFFFFFF)     0x08
;   +00         空描述符                                0x00

>****************************************************************************************<
内核程序 不仅要设置 自己用的段GDT段描述符:
;   +38         核心代码段(位于核心数据段之后)        0x38
;   +30         核心数据段(位于系统公用例程段之后)  0x30
;   +28         公用例程段(起始地址为00040000)        0x28

还要给用户程序相关段设置:

 ;建立程序头部段描述符
 ;建立程序代码段描述符
 ;建立程序数据段描述符
 ;建立程序堆栈段描述符
>****************************************************************************************<
[026][x86汇编语言]第十三章 学习内核程序 c13_core.asm_第2张图片
内核程序加载完全部用户程序各段描述符后的GDT示意图.png

5、建立程序堆栈段描述符

  • 代码
         ;建立用户程序堆栈段描述符
         mov ecx,[edi+0x0c]                 ;4KB的倍率
         mov ebx,0x000fffff
         sub ebx,ecx                        ;EBX = 段界限
         mov eax,4096
         mul dword [edi+0x0c]
         mov ecx,eax                        ;EAX=64位乘法结果
         call sys_routine_seg_sel:allocate_memory
         add eax,ecx                        ;得到堆栈的高端物理地址
         mov ecx,0x00c09600                 ;4KB粒度的堆栈段描述符
         call sys_routine_seg_sel:make_seg_descriptor
         call sys_routine_seg_sel:set_up_gdt_descriptor
         mov [edi+0x08],cx          ;回写
         
  • 说明
在子程序 load_relocate_program
里面有4个建立描述符的任务,分别是:
 ;建立程序头部段描述符
 ;建立程序代码段描述符
 ;建立程序数据段描述符
 ;建立程序堆栈段描述符

其中,用户程序的头部段、代码段、数据段都位于用户程序之中,
而 用户程序 会被 加载到 内核程序使用allocate_memory开辟的内存空间里之后,
这段内存空间从 0x0010 0000 开始 假设到 0xNNNN NNNN;

堆栈段不同上面3个段,内核程序会从 0xNNNN NNNN开始再开辟一段内存空间,
比如从0xNNNN NNNN 到 0xPPPP PPPP,把这段空间提供给用户程序当栈使用,
也就是说,用户程序不需要自己开辟空间来使用 堆栈,
用户程序用的栈空间是内核程序提供给它使用的;

回写还是一样的,在用户程序头部段,
0x08处的标号 stack_seg 处就用来填入 堆栈的段选择子,
这样用户程序想要使用堆栈的时候,
一样可以在用户程序自己的头部段里取到段选择子。

6、拿着用户程序SALT表中的条目 去 内核程序 一个一个地找

;位于子程序 load_relocate_program 

         ;重定位SALT
         mov eax,[edi+0x04]                 ;指向用户程序头部段
         mov es,eax                         
         
         mov eax,core_data_seg_sel          ;指向内核程序数据段
         mov ds,eax
         
         cld                                ;正向
         
         mov ecx,[es:0x24]                  ;用户程序SALT条目数
         mov edi,0x28                       ;用户程序内的salt位于头部段偏移地址0x28处
         
    .b2:
        push ecx
        push edi

                mov ecx,salt_items      ;这是内核程序里面现有的条目总数(是比用户程序多得多得)
                mov esi,salt            ;指向内核程序标号salt开始
        .b3:    
                push edi
                push esi
                push ecx
                             
                    ;---------------内核程序--------------------- 
                    ;salt_4           db  '@TerminateProgram'   ;
                    ;times 256-($-salt_4) db 0                  ;   
                    ;dd  return_point                           ;
                    ;dw  core_code_seg_sel                      ;
                    ;--------------------------------------------
                    mov ecx,64              ;检索表中,每条目的比较次数 64*4=256
                    repe cmpsd              ;每次比较4字节
                    jnz .b4                 ;如果不匹配就跳转到 .b4
                    ;-----------------用户程序-----------------------                   
                    ;TerminateProgram db  '@TerminateProgram'       ;
                    ;times 256-($-TerminateProgram) db 0            ;
                    ;------------------------------------------------                   
                    mov eax,[esi]           ;如果匹配,esi恰好指向入口地址
                    mov [es:edi-256],eax    ;将用户程序[es:edi-256] 改写成 偏移地址
                    mov ax,[esi+4]  
                    mov [es:edi-252],ax     ;将用户程序[es:edi_252] 改写成 段选择子
                    
        .b4:    
                pop ecx
                pop esi
                add esi,salt_item_len           ; 本质是256+6=262 条目256字节名词+6字节入口地址
                pop edi
                loop .b3         
         
        pop edi
        add edi,256                     ;查找下一个SALT条目
        pop ecx
        loop .b2

你可能感兴趣的:([026][x86汇编语言]第十三章 学习内核程序 c13_core.asm)