[Intel汇编-NASM]程序的加载以及硬盘访问

本文详细介绍了使用NASM汇编语言进行程序设计的方法,包括用户程序结构、段定义与属性、重定位表功能,以及如何实现加载器和应用程序头部来模拟操作系统的工作。通过具体实例,展示了程序加载过程、段的对齐方式、重定位表的作用,以及如何在程序中处理字符串输出和屏幕刷新。

 

转载地址:https://blog.youkuaiyun.com/lirx_tech/article/details/42680405

1. 用户程序的结构:

   1) 一般源程序都以段的形式进行组织,这样可以使逻辑更加清晰,在NASM中使用section关键字定义一个段,形式是:section 段名

   2) 程序可以用段名来引用段,但是NASM编译器并不关心段的具体用途,或者说是根本不知道段的用途(代码段还是数据段等),同时NASM对段的数量也没有任何限制,如果代码中没有定一段则整个程序自成一段;

   3) 定义段的同时可以定义段的一些属性,比如可以使用关键字align来定义段的对其方式,比如:section code align=16,这样就表示该段的其实地址是以16字节对齐的,即段的起始位置必须是16的整数倍;

!注意:该属性只影响段的起始位置的对其但不影响段的末尾对齐方式,事实上NASM也无法判断一个段的末尾,只有当遇到一个新的段的定义的时候才能知道前一个段结束了;

   4) 段的起始位置:就是该段中第一行指令的地址(指令可以是普通指令也当然可以是数据定义指令db、dw等等;

   5) 和加载程序之间的约定——应用程序头部Header:

       i. 在有操作系统的环境下编译完一个程序之后编译器会隐式地、默认地添加一个应用程序头部(位置处于程序的起始位置处);

       ii. 头部包含着加载器该如何加载该程序的一些信息,或者说是加载器和程序之间的某些约定或规范,而加载器通过这些信息将程序正确地加载进内存中;

       iii. 在有操作系统的环境下,应用程序头部和加载器都是操作系统负责的,但是在这里我们模拟一下这个操作系统的工作,即手写完成加载器和程序头部来模拟操作系统的这两个功能;

 

2. 用户程序和加载器的简单实现:

用户程序:默认程序已经正确加载到了内存的空闲位置,并且从定义的标号start处该是执行程序,作用是将两个数据段中的字符串打印到屏幕上,并且处理回车和换行两个控制符

!注意用户程序头部的定义,里面包含了程序大小、程序开始执行的入口、程序中各个段的起始位置等信息;

!其中重定位表的作用就是:编译后各项保存的是各段在源程序中的绝对汇编地址,经加载程序加载后就将各项修改成在物理内存中实际的地址,因此称为重定位表;

app.nas,编译后生成app.bin

; 应用程序头
; 用于提供加载器相关加载信息
; 是应用程序规范的一部分
section header vstart=0
    app_size        dd    app_end                    ; [APP_SIZE:0x00] 程序的大小(字节)
    app_entry        dw    start                    ; [APP_ENTRY:0x04] 入口处偏移地址
    app_entry_seg    dd    section.code1.start        ; [APP_ENTRY_SEG:0x06] 入口处段地址
    ; section.段名.start是NASM提供的伪指令,用于段起始位置在源程序中的绝对汇编地址
    ; 绝对汇编地址是指相对于整个源程序头的偏移量,而整个程序头的绝对汇编地址是0
    ; 绝对汇编地址是一个32位无符号数,因此使用dd表示

    c_realloc_tbl    dw    (tbl_end - tbl_start) / 4        ; [C_REALLOC_TBL:0x0A] 重定位表表项数目
tbl_start:    ; [TBL_START:0x0C]
    seg_addr_code1    dd    section.code1.start
    seg_addr_code2    dd    section.code2.start
    seg_addr_data1    dd    section.data1.start
    seg_addr_data2    dd    section.data2.start
    seg_addr_stack    dd    section.stack.start
tbl_end:
; section header end


;;
;;
section stack align=16 vstart=0
    resb 256
stack_end:
; section stack end


;;
;;
section data1 align=16 vstart=0
    msg0 db '  This is NASM - the famous Netwide Assembler. '
         db 'Back at SourceForge and in intensive development! '
         db 'Get the current versions from http://www.nasm.us/.'
         db 0x0d,0x0a,0x0d,0x0a
         db '  Example code for calculate 1+2+...+1000:',0x0d,0x0a,0x0d,0x0a
         db '     xor dx,dx',0x0d,0x0a
         db '     xor ax,ax',0x0d,0x0a
         db '     xor cx,cx',0x0d,0x0a
         db '  @@:',0x0d,0x0a
         db '     inc cx',0x0d,0x0a
         db '     add ax,cx',0x0d,0x0a
         db '     adc dx,0',0x0d,0x0a
         db '     inc cx',0x0d,0x0a
         db '     cmp cx,1000',0x0d,0x0a
         db '     jle @@',0x0d,0x0a
         db '     ... ...(Some other codes)',0x0d,0x0a,0x0d,0x0a
         db 0
; section data1 end


;;
;;
section data2 align=16 vstart=0
    msg1 db '  Welcome and enjoy NASM! '
         db '2015-01-05'
         db 0
; section data2 end


;;
;;
section code1 align=16 vstart=0
start:
            mov        ax, [seg_addr_stack]
            mov        ss, ax
            mov        sp, stack_end

            mov        ax, [seg_addr_data1]
            mov        ds, ax
            mov        bx, msg0
            call    put_string                    ; 显示第一段信息

            ; 在加载程序中将es指向header了
            push    word [es:seg_addr_code2]    ; 先将code2的偏移地址和段地址入栈
            mov        ax, _start.begin
            push    ax
            retf                                ; 利用retf修改cs:ip使其跳转至code2
    .continue:
            mov        ax, [es:seg_addr_data2]
            mov        ds, ax
            mov        bx, msg1
            call    put_string                    ; 使ds:bx指向msg1并输出

            jmp        $
; end start

; 字符串控制宏以及显卡光标端口宏
CHAR_TRAIL            equ        0x00        ; 字符串结束符
CHAR_RET            equ        0x0D        ; 回车符
CHAR_NL                equ        0x0A        ; 换行符
DCHAR_NONE            equ        0x0720        ; 显存中显示空的字

PORT_CHOOSE            equ        0x3D4        ; 索引端口,用于选择子端口(8位) 
SUBPORT_HIGH        equ        0x0E        ; 子端口号
SUBPORT_LOW            equ        0x0F        ; 这两个子端口分别存放光标位置的高位和低位
PORT_DATA            equ        0x3D5        ; 数据端口,存放选定的端口中的数据(8位)

VIDEO_SEG_BEGIN        equ        0xB800        ; 显卡区域起始段地址

; func put_string
; <- [ds:bx]:msg0
; colision register: es
; 将msg0打印至屏幕
put_string:
            push    es

            ; 获取当前光标位置保存在ax中
            mov        dx, PORT_CHOOSE
            mov        al, SUBPORT_HIGH        
            out        dx, al                    ; 选择一个子端口
            mov        dx, PORT_DATA
            in        al, dx
            mov        ah, al                    ; 从子端口中读取光标高位保存在ah中

            mov        dx, PORT_CHOOSE
            mov        al, SUBPORT_LOW
            out        dx, al
            mov        dx, PORT_DATA
            in        al, dx                    ; 同理从子端口中读取光标低位保存在al中
                                            ; 最终将整个结果保存在ax中

            ; 目前ax存放着光标的位置

    .lp:    mov        cl, [bx]                    ; 读取一个字符保存在cl中
            cmp        cl, CHAR_TRAIL                ; 判断该字符是否是结束符
            je        .ret
            call    put_char                    ; 不是结束符就打印该字符
            inc        bx                            ; 继续读取下一个字符
            jmp        .lp

    .ret:    pop        es
            ret

; func put_char
; <- cl:当前读取的一个字符
; colision register: ds, bx
put_char:    
            push    ds
            push    bx                        ; 备份

            ; ds和es都指向显卡
            mov        bx,    VIDEO_SEG_BEGIN
            mov        ds, bx
            mov        es, bx
            
            ; 目前ax存放着光标的位置

            cmp        cl, CHAR_RET            ; 判断字符是否是回车
            jne        .next0                    ; 不是回车则继续接下来的步骤
    .deal_ret: ; 是回车则处理回车
            mov        bl, 80
            div        bl
            mul        bl                        ; 除去光标位置中80的余数即可
                                            ; ax中得到的是回车后光标的位置
            jmp        .set_cursor

    .next0:    cmp        cl, CHAR_NL                ; 判断是否是换行符
            jne        .next1                    ; 如果不是换行符则继续接下来的代码
    .deal_nl: ; 处理换行的情形
            add        ax, 80                    ; 换行很简单,只要加80即可
            jmp        .deal_roll_screen        ; 换行可能会造成屏幕滚动,因此需要处理

    .next1:    ; 结束、回车、换行都不是那就是普通字符了,因此需要打印出来,并且光标后移一位
            mov        bx, ax                    ; 先将ax复制到bx中
            shl        bx, 1                    ; 显卡区域每个字符占两个字节(还有一个属性字节)
            mov        [bx], cl
            inc        ax                        ; 光标后移一位
            ; jmp    .deal_roll_screen        ; 光标后移也可能会造成滚屏

    .deal_roll_screen:
            cmp        ax, 2000            
            jl        .set_cursor                ; 检查光标是否越界,如果越界则需要滚屏,否则可以直接设置光标
        .roll_screen: ; 滚屏处理
            mov        si, 80 * 2
            mov        di, 0
            mov        cx, 2000 - 80
            cld
            rep        movsw
        .clear_bottom_line: ; 滚屏后需要清除最后一行
            mov        bx, (2000 - 80) * 2
            mov        cx, 80
        .cls:
            mov        word [bx], DCHAR_NONE
            add        bx, 2
            loop    .cls
            
            mov        ax, 2000 - 80        ; 滚屏后光标位置设置成最后一行起始
            ; jmp    .set_cursor            ; 滚屏完成后方可显示新的光标的位置了

    .set_cursor:
            mov        bx, ax                ; 将光标位置备份到bx中,因为访问端口会用到ax

            mov        dx, PORT_CHOOSE
            mov        al, SUBPORT_HIGH
            out        dx, al
            mov        dx, PORT_DATA
            mov        al, bh
            out        dx, al

            mov        dx, PORT_CHOOSE
            mov        al, SUBPORT_LOW
            out        dx, al
            mov        dx, PORT_DATA
            mov        al, bl
            out        dx, al

            pop        bx
            pop        ds

            ret
; section code1 end


;;
;;
section code2 align=16 vstart=0
_start:
    .begin:    push    word [es:seg_addr_code1]        ; code2没做什么实事就是再跳回code1的continue继续执行
            mov        ax, start.continue
            push    ax
            retf
; section code2 end

            
;;
;;
section trail align=16
app_end:
; section trail end
!resb指令就是reserve byte的缩写,即保留一定数量的字节的意思,因此必然还有resw、resd,表示保留一定数量的字和双字的意思,既然是保留就不对定义的数据进行初始化,因此该指令就是定义一段连续的未初始化的数据;
!关于汇编地址的介绍以及硬盘访问的端口都在源代码中详细介绍,所以这里就不累述了;

 

加载器的实现:作为主引导扇区程序

loader.nas,编译后生成loader.mbr

; 主引导扇区程序作为应用程序加载器

; 虽然就只有一个段但是也需要定义
; 最主要是为了使用段属性vstart=0x7C00
; 这样就可以使得段内的所有汇编地址都是相对0x7C00开始的
; 因为MBR加载在0x0000:0x7C00处,因此IP初始化为0x7C00
; 而所有偏移地址都是相对0x7C00的
; 有了这一步程序中的所有标号都能真正代表偏移地址了
section loader align=16 vstart=0x7C00
            jmp        near start

    LBA_APP_START        equ        100                ; 应用程序所在硬盘的起始逻辑扇区号,这里是人为规定的
    ADDR_20_LOAD_START    dd        0x10000            ; 内存中加载的起始20位绝对物理地址

    ; 应用程序头中信息的偏移地址
    APP_SIZE_LOW        equ        0x00        
    APP_SIZE_HIGH        equ        0x02
    APP_ENTRY            equ        0x04
    APP_ENTRY_SEG        equ        0x06
    APP_ENTRY_SEG_LOW    equ        0x06
    APP_ENTRY_SEG_HIGH    equ        0x08
    C_REALLOC_TBL        equ        0x0A
    TBL_START            equ        0x0C

            ; 从0x0FFFF往下(即地址减小)的一段区域一般都作为MBR的栈!
            ; 因此ss:sp指向0x0000:0x0000
            ; 这样在push的时候sp能回到0xFFFF
start:        mov        ax, 0
            mov        ss, ax
            mov        sp, ax

            ; ds -> 内存中加载的起始位置段地址
            mov        ax, [cs:ADDR_20_LOAD_START]
            mov        dx, [cs:ADDR_20_LOAD_START+2]
            mov        bx, 16
            div        bx
            mov        ds, ax
            mov        es, ax                        ; 留给用户程序时使ds和es都指向加载位置首部

            ; 先读取一个扇区,即应用程序头所在的扇区
            xor        di, di                        
            mov        si, LBA_APP_START            ; [di:si]全局保存当前读取的逻辑扇区号
            mov        cx, 1                        ; 读取一个扇区
            call    read_lba                
            ; 读取完毕,ds:0指向程序的第一扇区中的内容

            mov        dx, [APP_SIZE_HIGH]
            mov        ax, [APP_SIZE_LOW]
            mov        bx, 512
            div        bx
            cmp        dx, 0
            jne        .deal_left                ; 有余数,可以将已经读取的那个扇区看做余数的扇区
            dec        ax                        ; 无余数则需要减去已经读取的那个扇区
    .deal_left:
            cmp        ax, 0
            je        redirect_entry            ; 如果没有剩余扇区要读则直接去重定位程序入口点
            push    ds                        ; 备份并改变其指向

            mov        cx, ax                    ; 剩余要读的扇区数量
            mov        ax, ds
            add        ax, 0x20                ; 使其指向下一个512字节起始处(必然是16位对齐的)
            mov        ds, ax
            inc        si                        ; 指向下一个要读的扇区
            call    read_lba

            pop        ds                        ; 恢复ds使其指向加载的程序的开始处

        ; 到此为止程序彻底加载完毕
        
        ; 接下来的工作是将程序头中的入口地址,以及重定位表中的地址
        ; 修改成实际的物理地址
        ; 这里所重定位的地址都是段地址
        ; 将程序中段的绝对汇编地址更新成加载在内存中的实际物理段地址
        ; 公式是:16位物理段地址 = (整个程序起始位置的20位物理 + 段的32位绝对汇编地址) >> 4

    redirect_entry: ; 重定位入口处地址
            mov        dx, [APP_ENTRY_SEG_HIGH]        ; [dx:ax]中保存入口处的绝对汇编地址
            mov        ax, [APP_ENTRY_SEG_LOW]
            call    calc_seg_phy_addr_16            ; 计算段的16位段地址(即物理段地址),结果保存在ax中
            mov        [APP_ENTRY_SEG], ax                ; 更新

            ; 处理重定位表
            mov        cx, [C_REALLOC_TBL]
            mov        bx, TBL_START
    .realloc:
            mov        dx, [bx + 2]
            mov        ax, [bx]
            call    calc_seg_phy_addr_16
            mov        [bx], ax
            add        bx, 4
            loop    .realloc

            jmp        far [APP_ENTRY]                    ; 控制权交给应用程序


; func read_lba
; <- [di:si]:读取的逻辑扇区号
; <- cx:读取的扇区数量
; <- ds:目的区域段地址
; 将cx个扇区的内容读取到ds:0所指向的内存空间中
read_lba:
    PORT_DATA        equ        0x1F0        ; 数据端口(16位)
    PORT_ERRNO        equ        0x1F1        ; 错误端口(8位)保存最后一次执行命令后的状态(错误原因)
    PORT_CLBA        equ        0x1F2        ; 计数端口(8位)保存读写的扇区数量
    PORT_LBA_START    equ        0x1F3        ; 逻辑扇区号端口(32位共4个8位口)
                                        ; 低28位确定待操作的起始扇区号
                                        ; 最高的4位指定扇区寻址模式以及类型选择符)
    PORT_CTRL        equ        0x1F7        ; 控制端口(8位)下读写命令同时又能反映硬盘工作状态

    CTRL_READ        equ        0x20        ; 读命令,向控制端口发送

    BIT_MASK        equ        10001000B    ; 位掩码,取控制端口的第7位和第3位
                                        ; 第7位表示硬盘是否忙,1表示忙
                                        ; 第3位表示硬盘是否就绪,1表示就绪
    STATUS_READY    equ        00001000B    ; 彻底就绪时第7位是0,第3位是1,用于检测硬盘是否就绪

            ; 指定读取的扇区数量
            mov        dx, PORT_CLBA
            mov        al, cl
            out        dx, al

            ; 向LBA地址口写入28位逻辑扇区号

            mov        dx, PORT_LBA_START        ; 0~7位
            mov        ax, si
            out        dx, al

            inc        dx                        ; 8~15位
            mov        al, ah
            out        dx, al

            inc        dx                        ; 16~23位
            mov        ax, di
            out        dx, al

            inc        dx                        ; 24~27位
            mov        al, 111_0_0000B            ; ah保存24~27位,al中保存扇区寻址模式以及类型选择符
                                            ; 其中最高位的111表示采用28位逻辑扇区号模式
                                            ; 后面一位的0表示是主盘,1表示从盘,即盘片类型选择符
            or        al, ah
            out        dx, al

            ; 发出读命令
            mov        dx, PORT_CTRL
            mov        al, CTRL_READ
            out        dx, al
    .waits:    ; 检测硬盘是否就绪,没就绪就一直等待就绪
            in        al, dx
            and        al, BIT_MASK
            cmp        al, STATUS_READY
            jne        .waits

            ; 准备就绪就开始读取
            shl        cx, 8                    ; 一个扇区512B,即256个字
                                            ; cx记录剩余多少字未读完,而cx原本存放剩余扇区数
                                            ; 因此cx要乘以256,即左移8位
            mov        dx, PORT_DATA
            xor        bx, bx
    .readw: ; 循环读取程序,将其加载至ds:0处
            in        ax, dx
            mov        [bx], ax
            add        bx, 2
            loop    .readw

            ret


; func calc_seg_phy_addr_16
; <- [dx:ax]:段32位绝对汇编地址
; -> ax:16位物理段地址
calc_seg_phy_addr_16:
            ; 这里的20位起始加载地址使用32位保存的
            ; 因此可以通过带进位的加法得到段起始位置的实际的20位物理地址
            add        ax, [cs:ADDR_20_LOAD_START]
            adc        dx, [cs:ADDR_20_LOAD_START+2]

            ; 现在将绝对的20位物理地址右移4位就能得到16位的物理段地址了
            ; 必须dx和ax同时右移
            ; 方法是ax右移4位即可
            ; 而dx采用循环右移4位,应该移到ax高4位的那4位重新回到dx高4位
            ; 然后用位掩码去的dx高4位
            ; 再利用or将这4位写入ax的高4位即可
            shr        ax, 4            ; 低16位右移4位
            ror        dx, 4
            and        dx, 0xF000        ; 位掩
            or        ax, dx            ; 写入

            ret

times 510-($-$$) db 0
                 dw 0xAA55
!注意:程序开始处的一大堆宏定义就是用于和应用程序头部进行通讯的,即那些表项等在头部中的偏移位置,这样就可以轻松访问这些表项了;


3. 查看程序运行结果:将loader.mbr写进虚拟硬盘的0号扇区(总共一个扇区),将app.bin写进100号扇区(总共两个扇区),然后将虚拟盘作为虚拟机的启动盘放在VirtualBox中运行中即可;


————————————————
版权声明:本文为优快云博主「Lirx_Tech」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/lirx_tech/article/details/42680405

随书附送光盘内容一览表 光盘目录 对应的书目录 目标文件名 程序功能 09\JIAN-H 第9章的9.1 JIAN-H.EXE 建立汉字库头文件 09\HZCALL 第9章的9.2 HZCALL.OBJ 显示汉字程序模块 09\NAME 第9章的9.3 NAME.OBJ 图形方式下处理输入字符 10\ARSE 第10章的10.1 ARSE.EXE 读扇区数据(汇编) 10\CRSE 第10章的10.2 CRSE.EXE 读扇区数据(C) 10\HCRSE 第10章的10.3 HCRSE.EXE 读扇区数据(C、汉显) 11\AWSE 第11章的11.1 AWSE.EXE 扇区数据(汇编) 11\CWSE 第11章的11.2 CWSE.EXE 扇区数据(C) 11\HCWSE 第11章的11.3 HCWSE.EXE 扇区数据(C、汉显) 12\ALLSE 第12章的12.1 ALLSE.EXE 对扇区进行多种操作 12\HALLSE 第12章的12.2 HALLSE.EXE 对扇区进行多种操作(汉显) 13\READSF 第13章的13.1 READSF.EXE 读扇区备份文件 13\HREADSF 第13章的13.2 HREADSF.EXE 读扇区备份文件(汉显) 14\SEDIT 第14章的14.1 SEDIT.EXE 编辑扇区文件字节值 14\HSEDIT 第14章的14.2 HSEDIT.EXE 编辑扇区文件字节值(汉显) 15\SBLOCK 第15章的15.1 SBLOCK.EXE 扇区文件块拷贝 15\HSBLOCK 第15章的15.2 HSBLOCK.EXE 扇区文件块拷贝(汉显) 16\JIAN-H1 第16章的16.2.3 JIAN-H1.EXE 扩充汉字库头文件 16\HZCALL1 第16章的16.2.3 HZCALL1.OBJ 重新编译汉字显示程序模块 16\COMPSF 第16章的16.1 COMPSF.EXE 比较扇区文件 16\HCOMPSF 第16章的16.2 HCOMPSF.EXE 比较扇区文件(汉显) 17\0SE63 第17章的17.1 0SE63.EXE 显示0磁道扇区数据 17\H0SE63 第17章的17.2 H0SE63.EXE 显示0磁道扇区数据(汉显) 18\EARSE 第18章的18.2 EARSE.EXE 扩展读扇区数据(汇编) 19\EAWSE 第19章 EAWSE.EXE 扩展扇区数据(汇编) 20\RSECTOR 第20章的20.1-20.3 RSECTOR.EXE C调用汇编扩展读 20\HRSECTOR 第20章的20.4 HRSECTOR.EXE C调用汇编扩展读(汉显) 21\WSECTOR 第21章的21.1-21.3 WSECTOR.EXE C调用汇编扩展 21\HWSECTOR 第21章的21.4 HWSECTOR.EXE C调用汇编扩展(汉显) 22\EALLSE 第22章的22.1 EALLSE.EXE 对扇区多种扩展操作 22\HEALLSE 第22章的22.2 HEALLSE.EXE 对扇区多种扩展操作(汉显) 23\JIAN-H2 第23章的23.2 JIAN-H2.EXE 扩充汉字库头文件 23\HZCALL2 第23章的23.2
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值