第13章 程序的动态加载和执行

本文探讨了操作系统如何加载和执行用户程序,包括描述符表的使用、内核的初始化过程、用户程序的结构需求以及内核提供的例程。重点介绍了保护模式下,内核动态安装描述符和用户程序加载重定位的一般原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

操作系统需要考虑采用什么办法加载用户程序(所有的段,在使用前都要以描述符的形式定义在描述符表中),用户程序需要提供一些必要的信息帮助操作系统.

操作系统提供了大量的例程供用户使用,比如显示一个字符串,就不要让用户自己来写代码了,直接调用操作系统的代码即可.但操作系统系统和用户程序应当协商一种机制,让用户程序能够使用这些例程.(API)

内核不能放到主引导扇区(超过512字节),主引导程序加载内核,把控制权移交给它.内核加载用户程序,提供API给用户使用.

学习保护模式下,加载重定位用户程序的一般原理.

内核分四个部分初始化代码(主引导程序是初始化代码的部分),内核代码段,内核数据段,公共例程.

初始化代码 安装最基本的描述符,初始化执行环境,加载内核(从磁盘到内存)

初始化代码 内核的加载(主引导程序)

初始化代码,一开始就想进行保护模式,所以安装好gdt后,就直接进入保护模式了

  1. 安装描述符
  2. 进入保护模式
  3. 加载内核代码
  4. 动态安装描述符
    在这里插入图片描述
    为啥要动态安装: 因为内核的段的数据,代码,公共例程的长度不确定,起始地址不确定.
//主引导程序代码
         call make_gdt_descriptor
         mov [esi+0x28],eax
         mov [esi+0x2c],edx
//内核代码
         ;以下常量定义部分。内核的大部分内容都应当固定 
         core_code_seg_sel     equ  0x38    ;内核代码段选择子
         core_data_seg_sel     equ  0x30    ;内核数据段选择子 
         sys_routine_seg_sel   equ  0x28    ;系统公共例程代码段的选择子 
         video_ram_seg_sel     equ  0x20    ;视频显示缓冲区的段选择子
         core_stack_seg_sel    equ  0x18    ;内核堆栈段选择子
         mem_0_4_gb_seg_sel    equ  0x08    ;整个0-4GB内存的段的选择子
//这些段选择子的顺序是人为规定的,在写内核的时候可以引用这个段选择子.
//比如在内核代码中
         mov eax,mem_0_4_gb_seg_sel         ;切换DS到0-4GB的段
         mov ds,eax
内核代码

    以下是系统核心的头部,用于加载核心程序 
     core_length      dd core_end       ;核心程序总长度#00

     sys_routine_seg  dd section.sys_routine.start
                                        ;系统公用例程段位置#04

     core_data_seg    dd section.core_data.start
                                        ;核心数据段位置#08

     core_code_seg    dd section.core_code.start
                                        ;核心代码段位置#0c


     core_entry       dd start          ;核心代码段入口点#10
                      dw core_code_seg_sel

声明常数,内存段的选择子,它们对应的描述符会在内核初始化的时候创建.内核代码知道每个段选择子的具体数值(选择子要和对应的段对应上),对应于引导程序的第4步, 动态安装描述符.

头部 // 核心程序总长度#00
记录整个文件的大小
记录各个段的汇编偏移量(系统公用例程段位置,核心数据段位置,核心代码段位置)
内核入口点(段内汇编偏移量,段选择子[前面的常数])


[bits 32]
SECTION sys_routine vstart=0                ;系统公共例程代码段 

SECTION core_data vstart=0                  ;系统核心的数据段

SECTION core_code vstart=0
用户程序

用户的程序要有一定的结构才能被内核加载

  • 实际上即始是大多数汇编语言也不需要亲自构造头文件,那是链接器的工作.但是链接器是为流行的操作系统服务的,用于构造他们可以识别的可执行文件格式.
  • 在流行的操作系统里,内存管理是一项重要又严肃的工作,它要记住所有可以分配的内存,将它们分成块,当要求分配内存时,内存管理程序将查找并分配那些空闲块.当占用这些块的用户终止执行后,还要负责回收它们,以便再用于分配.当内存紧张时,通过磁盘来换出不常用的块.

SECTION header vstart=0

     program_length   dd program_end          ;程序总长度#0x00
     
     head_len         dd header_end           ;程序头部的长度#0x04

     stack_seg        dd 0                    ;用于接收堆栈段选择子#0x08
     stack_len        dd 1                    ;程序建议的堆栈大小#0x0c
                                              ;以4KB为单位
                                              
     prgentry         dd start                ;程序入口#0x10 
     code_seg         dd section.code.start   ;代码段位置#0x14
     code_len         dd code_end             ; 

     data_seg         dd section.data.start   ;数据段位置#0x1c
     data_len         dd data_end             ;数据段长度#0x20
     程序长度

程序的头部长度
栈选择子,内核不要求用户程序提供栈空间,而是由内核动态分配,以减轻用户编写的负担.内核空间分配好栈后,会把选择子写在这.
//内核中的代码,把选择子写在这
mov [edi+0x14],cx
//这个用户代码的头部一定要是这样的
jmp far [0x10] ;控制权交给用户程序(入口点)

栈大小1=4kb,2=8kb
程序入口的32位偏移地址(在这个段中)
代码段的位置(在整个汇编中),当内核对用户的程序加载重定位后,把该段的选择子回写到这里(仅占低字节部分),这样一来,它和上面的一起组成6字节程序的入口.内核从这里转移控制权到程序
代码段的长度
数据段同上


   符号地址检索表

SECTION data vstart=0

     buffer times 1024 db  0         ;缓冲区

     message_1         db  0x0d,0x0a,0x0d,0x0a
                       db  '**********User program is runing**********'
                       db  0x0d,0x0a,0
     message_2         db  '  Disk data:',0x0d,0x0a,0

data_end:


SECTION code vstart=0
start:

    ;下面这两句可能理解为规范,用户程序的开头就应该这样写
     mov eax,ds   ;ds的内核中被指向了用户的头选择子.
     mov fs,eax   ;保存自己的头选择子.
 
     mov eax,[stack_seg]  ;读取自己的段选择子
     mov ss,eax
     mov esp,0
 
     mov eax,[data_seg]  ;读取自己的数据选择子
     mov ds,eax
 
     mov ebx,message_1
     call far [fs:PrintString]
 
     mov eax,100                         ;逻辑扇区号100
     mov ebx,buffer                      ;缓冲区偏移地址
     call far [fs:ReadDiskData]          ;段间调用,[fs:ReadDiskData] 里面的内容是内核定义的代码的 
     选择子和偏移量, 可以直接跳到对应位置对执行
 
     mov ebx,message_2
     call far [fs:PrintString]
 
     mov ebx,buffer 
     call far [fs:PrintString]           ;too.
 
     jmp far [fs:TerminateProgram]       ;将控制权返回到系统 

看这章的代码
  • 内核和引导程序其实是一起的.它们两被分开是因为bios只读取硬盘的第一个扇区并执行.所以在写的时候,内核代码常常去引用 引导程序定义的选择子.而用户程序又是和引导程序,内核代码完全分开的.用户程序不知道内核.所以用户代码的开头会读取自己的栈选择子,数据选择子.用户程序位于第50扇区这是内核写好的.
  • 内核要提供一些例程供用户程序调用.但它们在操作系统内部,对任何人来说都是不可见的,call调用要直接或间接地址.如果地址写死,也会有问题,操作系统升级后地址也会有变化.早期是通过API中断号来公开它们,另一种方式是使用符号名.(方法名),但不会列出段地址和偏移地址在操作系统开发手册中,会列出所有的符号名,符号名在高级语言里就是库函数名.要求用户在0x28的地方构造一个表格,在表格中列出要用到的符号名,256字节,不足用0x00填充.在用户程序加载后,内核会分析这个表格,并将每一个符号名替换成相应的内存地址.这就是重定位过程 (符号地址检索表)
//调用指向这个表对应函数名,从这个地址中读取内核代码提供的函数的 段选择子和偏移量
call far [fs:PrintString]
  • 汇编语言写东西太难看懂了
//用户程序一开头就写这个,那么ds是什么?   看要看内核,ds是内核给设置的,用户程序对应的选择子
         mov eax,ds
         mov fs,eax
//用户内核的部分代码
         mov esi,50                          ;用户程序位于逻辑50扇区 
         call load_relocate_program
      
         mov ebx,do_status
         call sys_routine_seg_sel:put_string
      
         mov [esp_pointer],esp               ;临时保存堆栈指针
       
         mov ds,ax         

//看这段代码你需要看 load_relocate_progra的函数说明,但是没有变量名的帮助,真的是记不住
;加载并重定位用户程序               
;输入:ESI=起始逻辑扇区号           
;返回:AX=指向用户程序头部的选择子  
其它内容
//内核定义了符号地址检索表,  名称,偏移地址和段选择子
//实际上是 别名,偏移地址和段选择子
         ;符号地址检索表
         salt:
         salt_1           db  '@PrintString'
                     times 256-($-salt_1) db 0
                          dd  put_string
                          dw  sys_routine_seg_sel
 //用这个表和用户头部定义的要使用的函数对比,替换成相应的内容.        
 cld 清空方法正向 cli关闭中断
 rep,repz,repe
                  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值