[026][x86汇编语言]第十三章|[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 ebxmov eax,core_data_seg_sel mov ds,eaxmov eax,[ram_alloc]; 标号ram_alloc此时存着本次分配的起始线性地址 add eax,ecx; 起始地址加上要分配的字节数形成下一次分配的起始地址mov ecx,[ram_alloc]; 返回本次分配的起始地址; 强制起始地址是4字节对齐的 mov ebx,eax; 将eax的值备份到ebxand ebx,0xfffffffc; 0xfffffffc=1111_1111_1111_1111_1111_1111_1111_1100B add ebx,4; 0x4 = 0100Btest eax,0x00000003; 0x00000003=0000_0000_0000_0000_0000_0000_0000_0011B cmovnz eax,ebx; 如果没有对齐则采用强制对齐后的数值ebx,否则保持原样mov [ram_alloc],eax; 将可用于下一次分配的起始地址回写到标配ram_allocpop ebx pop eax pop dsretf; 段间调用; -------------------------------------------------------------------------------

  • 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汇编语言]第十三章|[026][x86汇编语言]第十三章 学习内核程序 c13_core.asm
    文章图片
    内核程序 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_memorymov 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,eaxmov 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 edxpush ds push esmov ebx,core_data_seg_sel; 切换到内核数据段 mov ds,ebx; -------------------------------------------------------- ; pgdtdw0; 用于设置和修改GDT; ; dd0; ; -------------------------------------------------------- sgdt [pgdt]; 将GDTR寄存器的基地址和界限值保存到pdgt处 ; GDT的段基地址自 加载程序 以来 一直是0x007E 0000 ; -------------------------------------------------------- ; pgdtdwN; 用于设置和修改GDT; ; dd0x007E000; ; --------------------------------------------------------mov ebx,mem_0_4_gb_seg_sel; 指向0~4GB内存的段的选择子 mov es,ebxmovzx 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 dspop edx pop ebx pop eaxretf

  • 联系一下 加载程序 c13_mbr.asm
【[026][x86汇编语言]第十三章|[026][x86汇编语言]第十三章 学习内核程序 c13_core.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]; -------------------------------------------------------------------------- ; 表 ; -------------------------------------------------------------------------- pdgtdw 0 dd 0x00007e00; GDT的物理地址

  • 再看本文的 内核程序 c13_core.asm 中相关语句,也是2条指令,分别是:
读取GDTR 寄存器内容的 sgdt sgdt [pgdt]; 将GDTR寄存器的基地址和界限值保存到pdgt处 ; GDT的段基地址自 加载程序 以来 一直是0x007E 0000 ; -------------------------------------------------------- ; pgdtdwN; 用于设置和修改GDT; ; dd0x007E000; ; -------------------------------------------------------->****************************************************************************************< 修改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 ; +080~4GB数据段(00000000~FFFFFFFF)0x08 ; +00空描述符0x00>****************************************************************************************< 内核程序 不仅要设置 自己用的段GDT段描述符: ; +38核心代码段(位于核心数据段之后)0x38 ; +30核心数据段(位于系统公用例程段之后)0x30 ; +28公用例程段(起始地址为00040000)0x28还要给用户程序相关段设置: ; 建立程序头部段描述符 ; 建立程序代码段描述符 ; 建立程序数据段描述符 ; 建立程序堆栈段描述符 >****************************************************************************************<

[026][x86汇编语言]第十三章|[026][x86汇编语言]第十三章 学习内核程序 c13_core.asm
文章图片
内核程序加载完全部用户程序各段描述符后的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,eaxmov eax,core_data_seg_sel; 指向内核程序数据段 mov ds,eaxcld; 正向mov ecx,[es:0x24]; 用户程序SALT条目数 mov edi,0x28; 用户程序内的salt位于头部段偏移地址0x28处.b2: push ecx push edimov ecx,salt_items; 这是内核程序里面现有的条目总数(是比用户程序多得多得) mov esi,salt; 指向内核程序标号salt开始 .b3: push edi push esi push ecx; ---------------内核程序--------------------- ; salt_4db'@TerminateProgram'; ; times 256-($-salt_4) db 0; ; ddreturn_point; ; dwcore_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 .b3pop edi add edi,256; 查找下一个SALT条目 pop ecx loop .b2

    推荐阅读