学习笔记
《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
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
还要给用户程序相关段设置:
;建立程序头部段描述符
;建立程序代码段描述符
;建立程序数据段描述符
;建立程序堆栈段描述符
>****************************************************************************************<
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