[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
文章图片
内核程序 allocate_memory ram_alloc dd 0x0010000.png
test
是做and运算
,但是不改变寄存器结果;
cmovnz eax,ebx
,如果运算结果不为零(说明eax末两位不是00,即没有对齐),就用ebx的值覆盖eax的值;
- 第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/49cbc41617993、子程序: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还要给用户程序相关段设置: ;
建立程序头部段描述符
;
建立程序代码段描述符
;
建立程序数据段描述符
;
建立程序堆栈段描述符
>****************************************************************************************<
文章图片
内核程序加载完全部用户程序各段描述符后的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
推荐阅读
- 026-Catagory-NSString
- D026+1组溪悦+《天长地久,给美君的信》读书笔记
- #|阿尔法点亮LED灯(一)汇编语言
- x86架构应用如何向Arm架构低成本迁移
- android x86虚拟机 网络正确配置
- 区块链精进手册|区块链精进手册 | 026 | 大师的投资思想(7)(理解风险管控)
- 操作系统内核实现|基于X86架构的OS内核设计之杂记(三)
- 操作系统内核实现|基于X86架构的OS内核设计之杂记(一)
- 基于X86架构的OS内核设计之杂记(二)
- 汇编语言(3) 条件处理