美文网首页
[012][x86汇编语言]加载程序与用户程序

[012][x86汇编语言]加载程序与用户程序

作者: AkuRinbu | 来源:发表于2018-05-23 22:32 被阅读0次

源程序

  • 加载程序,最少要提供把✔用户程序从硬盘到内存的加载功能以及✔跳转到用户程序执行的功能;
  • 用户程序,最少要提供✔用户程序长度以及✔程序入口的数据,给加载程序;

其余的代码都是支撑这些必需功能的具体代码实现,"要做的什么"不能变,具体实现"怎么做的"方法可以自己选;

加载程序

https://www.jianshu.com/p/add4f948a321

加载程序的组成部分:
  循环读取扇区部分
  跳转到用户程序

用户程序

https://www.jianshu.com/p/4177e9ff49d6

用户程序的组成部分
1、头部段(用户程序长度,用户程序入口地址,需要重定位的表项数,段重定位表项)
2、功能段(代码段、数据段)

加载程序的组成

1、设置段寄存器(属于具体的代码实现,为加载程序编写代码服务)

  • 1.0 CPU充电后,硬盘主引导扇区的加载程序被加载到内存0x0000:0x7C00处开始执行,此时 CS= 0x0000,加载程序里全部的标号都要加上0x7c00,因此设置vstart=0x7c00
    SECTION mbr align=16 vstart=0x7c00

  • 1.1 栈段寄存器SS与栈指针SP
    用于加载程序中子程序调用的压栈操作

  • 1.2 数据段寄存器DS

DS的初始值,
是由 物理地址0x10000 通过(子程序calc_segment_base )计算而来的
逻辑(段地址:偏移地址)中的段地址 0x1000

-----------------------------------------------------------------------------
在【循环读取扇区部分】,借由压栈保护,DS得以复用:
此时,DS将在每读取一个新扇区(512字节)的内容时,被赋予新的段地址

应该这样理解,
整个(用户程序)开头的“512个字节”会被加载到 段地址:偏移地址=0x1000:0x0000
之后如果还有内容,之后的又“512个字节”会被加载到 段地址:偏移地址=0x1020:0x0000(512D=20H)

每读取1个扇区(512字节)的用户程序到内存,就会得到下一个以512字节为边界的段地址,
在代码上的实现就是 段地址+0x20

-----------------------------------------------------------------------------
在【回写逻辑段地址部分】,DS一直使用的就是这个初始值0x1000,
为的是指向(用户程序)的 头部段header段

对(用户程序)而言,整个代码,就是从头部段header段开始的,
因此,对于DS的初始值而言,这既是整个(用户程序)的段地址,
header段的段地址;
既然是header段的段地址,在使用了限定关键词 vstart = 0后,
位于header段内的标号,这个标号代表的偏移量,就成为可以访问到这个标号处的偏移地址。

  • 1.3 其他段寄存器ES
初始值同DS寄存器,为0x1000
加载程序中没有用到,是冗余代码

2、循环读取扇区部分

  • 2.1 加载程序负责将位于硬盘的用户程序加载到内存
-----------------------------------------------------------------------------
此时,加载程序 在哪里?
-----------------------------------------------------------------------------
加载程序位于内存,CPU正在读取它的指令;





-----------------------------------------------------------------------------
用户程序 在哪里?(源地址)
用户程序 在硬盘,在【硬盘LBA模式逻辑扇区号100】处;
-----------------------------------------------------------------------------
  扇区号虽然这里只是一个十进制数100,但是本质上却是一个28位数,需要使用两个16位寄存器来组成
  di ,组成28位逻辑扇区号的高12位
  si ,组成28位逻辑扇区号的低16位

  由于现在这里的扇区号是十进制数的100,高12位明显是零,所以
   xor di,di                   ;28位起始逻辑扇区号的高12位 全是零
   mov si,app_lba_start        ;28位起始逻辑扇区号的低16位 设为100
  (搭配全局变量  app_lba_start equ 100 )


-----------------------------------------------------------------------------
用户程序 要被放到哪里去?(目的地址)
-----------------------------------------------------------------------------
用户程序 要被放到 内存的空闲空间去,【内存物理地址0x10000 】处刚好空闲
(加载程序知道这里是空闲的所以放到这里);

  • 2.2 根据(源地址)和(目的地址)读用户程序的第一个扇区(读硬盘到内存): 调用一次call read_hard_disk_0
一次 call read_hard_disk_0 调用
即一次读扇区操作,包括:
(1)设置要读取的扇区数;
(2)解读LBA模式扇区号;
(3)请求硬盘读操作;
(4)查询硬盘状态;
(5)完成连续读取数据。
一次 call read_hard_disk_0 调用
即一次读扇区操作,包括:
(1)设置要读取的扇区数;
            mov dx,0x1f2    ; 0x1f2 
            mov al,1        ; 扇区数   
            out dx,al


(2)解读LBA模式扇区号:28位依次对号入座,固定写法
            inc dx          ; 0x1f3
            mov ax,si       
            out dx,al       ; LBA地址7~0

            inc dx          ; 0x1f4
            mov al,ah
            out dx,al       ; LBA地址15~8
            
            inc dx          ;0x1f5
            mov ax,di
            out dx,al       ; LBA地址23~16
            
            inc dx          ; 0x1f6
            mov al,0xe0     ; LAB 主硬盘
            or al,ah
            out dx,al

(3)请求硬盘读操作;
            inc dx          ; 0x1f7 [命令端口]
            mov al,0x20     ; 读命令
            out dx,al

(4)查询硬盘状态;
 .waits: 
            in al,dx        ; 0x1f7 [状态端口]
            and al,0x88     ; 1000 1000 保留 BSY ... DRQ...
            cmp al,0x08     ; 0x08 硬盘已准备好与主机交换
            jnz .waits

(5)完成连续读取数据。
            mov cx,256      ; 总共要读取的字数256字=512字节
            mov dx,0x1f0    ; 0x1f0 [数据端口]
    .readw: 
            in ax,dx        ; 在子程序调用前已经清零xor bx,bx
            mov [bx],ax     ; 指定数据段 DS 指向用户程序目标地址的段地址
            add bx,2        
            loop .readw

    1个扇区有512个字节,就是256个字
    每一个新的扇区就是一个新的段,
    每一个新的段都要从偏移地址0x0000开始,
    这里使用 [BX] 来寻址内存,本质是DS:[BX],

    如果是加载 (用户程序)的第一个扇区内容(开头的512字节),
    那么自然 DS = 0x1000,而 BX从0x0000开始遍历;

    可以看到,对于(用户程序)而言,
    第一个 SECTION 分段里的字节数是远远不到512字节的,
    而我们所说的读1个扇区,是要读结结实实的512个字节;

    因此,读(用户程序的)第一个扇区不止读到(用户程序)第一个SECTION部分的内容, 
    更会往下读满程序机器码512个字节。
  • 2.3 读用户程序的其余内容(读硬盘到内存): 反复多次调用 call read_hard_disk_0
在上方的读入第一个扇区操作后,
用户程序的头部段全部内容在内的用户程序的头512个字节已经被读入了内存,
位于内存 段地址:偏移地址 = 0x1000:0x0000 开始 到 0x1000:0x01FF 结束

因此,可以直接通过访问内存特定位置来拿到用户程序头部段内的数据:

用户程序的头部段,包括
(1)用户程序长度;
(2)用户程序入口地址;
(3)需要重定位的表项数;
(4)段重定位表项;

根据用户程序长度,多次调用 read_hard_disk_0  完成用户程序全部机器码的从硬盘到内存的读写
======================= 用户程序 ======================
用户程序的头部段,包括

SECTION header vstart=0
(1)用户程序长度;
    program_length  dd  program_end     ;程序总长度[0x00]

(2)用户程序入口地址;
    code_entry      dw  start                   ;偏移地址[0x04] 
                    dd  section.code_1.start    ;段地址[0x06] 

(3)需要重定位的表项数;
realloc_tbl_len dw  (header_end - code_1_segment)/4 ;[0x0a]

(4)段重定位表项;
    code_1_segment  dd  section.code_1.start    ;[0x0c]
    code_2_segment  dd  section.code_2.start    ;[0x10]
    data_1_segment  dd  section.data_1.start    ;[0x14]
    data_2_segment  dd  section.data_2.start    ;[0x18]
    stack_segment   dd  section.stack.start     ;[0x1c]

================= 加载程序 ===================================
根据用户程序长度,就可以计算出需要读取的剩余全部机器码字节数,
折算成扇区数,完成剩余扇区的循环读取,实现用户程序从硬盘到内存的全部加载:

        mov dx,[2]      ; 32位用户程序长度的高16位
        mov ax,[0]      ; 32位用户程序长度的低16位
        mov bx,512      ; 1个扇区512字节
        div bx
        cmp dx,0        ; dx里存着余数,余数不为0代表没有除尽
        jnz @1
        dec ax
    @1:
        cmp ax,0        ; 小于1个扇区或者长度为512的整数倍时ax = 0
        jz direct
        
        ; 读取剩余的扇区
        push ds         ; 用户程序的开头是基于LBA逻辑扇区号计算出来的段地址
        
        mov cx,ax       ; 循环次数(剩余的扇区数)
    @2: 
        mov ax,ds       
        add ax,0x20     ; 512D = 0x20
        mov ds,ax       ; 得到下一个以512字节为边界的段地址
        
        xor bx,bx       ; 新的一段开始,偏移地址都是从0x0000开始
        inc si          ; 新的LBA逻辑扇区号,最初的是app_lba_start
        call read_hard_disk_0       ; 不重新设置di,是因为di不需要修改,di = 0
        loop @2         ; 循环读,直到读完整个功能程序(即用户程序)
        
        pop ds          ; 恢复数据段基址到用户程序头部段

3、回写逻辑段地址部分(属于具体的代码实现,与用户程序紧密相关,为用户程序具体代码编写服务)

  • 3.1 通过直接访问内存拿出用户程序头部段的数据
  • 3.2 用户程序结构
; 用户程序
;----------------------------------------------------------------------
SECTION header vstart=0                 ;定义用户程序头部段
    program_length  dd  program_end     ;程序总长度[0x00]
    
    ;用户程序入口点
    code_entry      dw  start                   ;偏移地址[0x04] 此处的start来源于自己的命名
                    dd  section.code_1.start    ;段地址[0x06] 此处.start是汇编指令的语法
                        
    realloc_tbl_len dw  (header_end - code_1_segment)/4
                                                ;段重定位表项个数[0x0a]
                                                ;1个表项占4个字节
    ;段重定位表项
    code_1_segment  dd  section.code_1.start    ;[0x0c]
    code_2_segment  dd  section.code_2.start    ;[0x10]
    data_1_segment  dd  section.data_1.start    ;[0x14]
    data_2_segment  dd  section.data_2.start    ;[0x18]
    stack_segment   dd  section.stack.start     ;[0x1c]
    
    header_end:
;----------------------------------------------------------------------
SECTION code_1 align=16 vstart=0    ;定义代码段1(16字节对齐)

;----------------------------------------------------------------------
SECTION code_2 align=16 vstart=0    ;定义代码段2(16字节对齐)

;----------------------------------------------------------------------
SECTION data_1 align=16 vstart=0

;----------------------------------------------------------------------
SECTION data_2 align=16 vstart=0

;---------------------------------------------------------------------- 
SECTION stack align=16 vstart=0 

;---------------------------------------------------------------------- 
SECTION trail align=16

  • 3.3 结合(目的地址)调用 calc_segment_base 将SECTION 标记的段地址 计算成 可以映射真实物理地址的 逻辑段地址
================= 用户程序 ====================
举例说明,
对 段code_1 而言,

SECTION code_1 align=16 vstart=0  

 vstart=0   说明这个段内的标号代表的偏移量全是针对这个段开始计算的(十分关键!!!)

---------------------------------------------------------------
code_1_segment  dd  section.code_1.start    ;[0x0c]
section.code_1.start 是 段code_1 相对整个用户程序开头的汇编地址
----------------------------------------------------------------
现在用户程序在内存中的真实物理地址是0x10000,计算而来的逻辑段地址是0x1000,

要结合这个逻辑段地址0x1000 以及 section.code_1.start ,
把后面code_1_segment 后面的那个 dd 型数据 
也换算成可以映射真实物理地址的逻辑段地址,
这样就 用户程序 可以通过访问头部段的标号 来拿到每个 SECTION 在内存中的逻辑段地址;

================== 加载程序 ======================
calc_segment_base:          ;计算16位段地址
                            ;输入 DX:AX = 32位物理地址
                            ;返回 AX = 16位段基地址
            push dx
            
            add ax,[cs:phy_base]
            adc dx,[cs:phy_base+0x02]       ; 用户程序开头的物理地址是0x10000位于标号phy_base处
            shr ax,4
            ror dx,4
            and dx,0xf000
            or ax,dx
            
            pop dx
            
            ret

物理地址 转换为 段地址
就是除以 十进制数 16 (即 除以十进制数10,即 循环右移4位)

add 以及 带进位 adc 加法
把用户程序开头的物理地址0x10000 与 用户程序中某个SECTION 段的汇编地址相加,
得到的结果再循环右移4位(完成除法)。



----------------------------------------------------------------

所有需要重新定位的SECTION 都放入重定位表里 

=================== 加载程序 =====================
      ;计算入口点代码段基址
    direct:             ; ds 指向用户程序头部段
        mov dx,[0x08]
        mov ax,[0x06]   ; 用户程序 "code_1段"(代码段) 相对于用户程序开头的 偏移量
        call calc_segment_base  ; 结合用户程序目的地址,计算 "code_1段"(代码段) 的 段地址
        mov [0x06],ax   ; 将 "code_1段"(代码段) 的段地址 回写
        
        ; 开始处理段重定位表
        mov cx,[0x0a]   ; 需要重定位的表项数
        mov bx,0x0c     ; 需要重定位表项相对用户程序开头的偏移量
        
    realloc:    
        mov dx,[bx+0x02]    ; 32位表项偏移量,高16位
        mov ax,[bx]         ; 32位表项偏移量,低16位
        call calc_segment_base  ; 结合用户程序目的地址, 计算 表项 的段地址
        mov [bx],ax
        add bx,4            ; 下一个重定位项 (每项占4个字节)
        loop realloc
    
==============================================
回写完成之后,
code_1_segment  dd  section.code_1.start 
这个标号code_1_segment 之后的数据再也不是一个针对用户程序开头的汇编地址了,
而是一个可以映射真实物理地址的逻辑段地址。

这个逻辑段地址是 段code_1 在内存中的段地址,不妨假设为A,
并且由于 SECTION code_1 align=16 vstart=0 这里的 vstart=0,
再假设 段code_1 里有一个标号 B,
那么标号start 在内存中的 段地址:偏移地址 就是 = A:B,
这个标号相对 段code_1的偏移量就是偏移地址。

4、跳转到用户程序

  • jmp far [0x04]
此时,DS是初始值0x1000,指向用户程序的开头,
[0x04] = DS:[0x04]
内存 0x1000:0x0004 处的汇编指令是 用户程序的

 code_entry      dw  start                   ;偏移地址[0x04] 此处的start来源于自己的命名
                 dd  section.code_1.start    ;段地址[0x06] 此处.start是汇编指令的语法

============================================================================================================================
jmp far 是 16位间接绝对远转移 指令
使用  jmp far [BX] ,从指令给出的偏移地址处取出两个字,分别是偏移地址和段地址

============================================================================================================================

由于之前的回写
 code_entry      dw  等于 B(标号start相对段code_1的汇编地址)
                         dd  等于 A(段code_1可以映射真实物理地址的逻辑段地址)
============================================================================================================================

jmp far 会使得 CS = A IP =B
本质上就是跳转到 用户程序 code_1段的 标号 start 处

相关文章

  • 汇编练习:关于VirtualBox中出现“no bootable

    问题 最近在看《x86汇编语言 从实模式到保护模式》,书中使用了Virtualbox进行裸机的虚拟以便执行汇编程序...

  • 汇编语言 学习笔记(三)

    汇编语言学习笔记 四、汇编语言程序格式 语句基本格式 汇编语言程序中的语句由 4 项组成,格式如下: 名字(nam...

  • Python-part3-基础

    与用户交互 1、什么是与用户交互 程序等待用户输入一些数据,然后程序执行完毕后为用户反馈信息 2、为何程序要与用户...

  • LINUX系统调用

    系统调用是用户程序和linux内核交互的接口,linux的系统调用有下面三种方式: int 80 在x86与x86...

  • 【笔记】《Linux C编程一站式学习》第17章 x86汇编程序

    第17章 x86汇编程序基础 本文是看17章的笔记。方便自己回顾。汇编基础知识可以看王爽的《汇编语言》本文环境我用...

  • 基本执行结构和存储程序原理

    从程序到电子信号: 高级语言源程序-->(编译)-->汇编语言程序-->(汇编)-->目标文件:机器语言模块 & ...

  • 深入计算机系统---程序的机器级表示

    一、汇编语言与机器语言简单介绍 简单讲 二、程序编码以及数据格式 程序编码,数据格式 我们在写 C 程序时,处理器...

  • 想象一下搭载5G的真小(大)程序?

    微信小程序分包加载功能上线 【青否小程序】为了进一步提高加载流畅性,打造媲美 App 的用户体验。小程序对开发者的...

  • 基础知识

    程序的生成过程:预处理:#define,include编译:生成汇编语言程序汇编:生成机器代码链接:动态链接(程序...

  • 汇编语言

    基础知识: 汇编语言的主体是汇编指令,它决定了汇编语言特性 程序员用汇编语言写出源程序,再用汇编编译器将其编译成机...

网友评论

      本文标题:[012][x86汇编语言]加载程序与用户程序

      本文链接:https://www.haomeiwen.com/subject/fikejftx.html