NASM(五)加载用户程序及用户程序


用户加载程序

  • 0x0FFFF以下,是加载器及其栈的使用空间;物理地址A0000以上,是BIOS和外围设备的使用空间,有很多传统的老式设备将自己的存储器和只读存储器映射到这个空间。如此一来,用户程序的可用的空间就位于0x10000-0x9FFFF,因此我们将用户程序加载至物理地址0x10000处。
  • 程序最初初始化栈段,并读取第一个LBA扇区。磁盘读写见 NASM(三)I/O端口和端口访问,此处修改了代码,使其能够一次性读多个扇区。
  • 通过读写的扇区信息,确定程序长度,继续将程序全部读出。
  • 程序读出后,需要对用户程序进行段重定位。

    用户程序段重定位,在编译阶段,编译器为每个段计算了一个汇编地址。第一个段header位于整个程序的开头,所以其汇编地址为0。从第二个段开始,每个段的汇编地址都是其相对于整个程序开头的偏移量,以字节为单位。因为我们不知道各个段的汇编地址到底是多少,故用字母来表示。这样,第二个段code_1的汇编地址是v,第三个段code_2的汇编地址是w,以此类推,最后一个段stack的汇编地址是z。现在,用户程序已经全部加载到内存里了,而且是从物理地址phy_base开始的。如此一来,每个段在内存中的物理地址都是基于phy_base的,第一个段header在内存中的起始物理地址是phy_base(phy_base+0),第二个段在内存中的起始物理地址是phy_base+v,以此类推,最后一个段stack则是phy_base+z.
  • 程序重定位完成,使用jmp far指令跳转到用户程序执行
  • 其余说明见注释。
    app_lab_start equ 100

section mbr align=16 vstart=0x7c00
    mov ax, 0
    mov ss, ax
    mov sp, ax  ; 栈 初始化
    mov ds, ax

    ; 准备读,看read_disk_one_sector注释
    mov ax, [phy_base]  
    mov dx, [phy_base + 0x02]  ; 将程序加载地址保存至ax,dx中
    mov bx, 16  ; 注意不能用逻辑右移指令,dx中可能有值
    div bx
    mov ds, ax  
    mov es, ax  ; es段指向ax
    mov ax, app_lab_start
    mov cx, ax  ; LBA逻辑扇区号低位存储在cx
    shr ax, 16  ; 拿到高位 (没用,mov时溢出就截断了)
    mov bx, ax  ; LBA逻辑扇区号高位存储在bx
    mov di, 0
    mov ax, 1
    call read_disk_sector ; 读LBA逻辑扇区号100,读取的数据存储在0x10000,读取1个扇区
    mov dx, [2] ; es:di不允许变动
    mov ax, [0] 
    mov bx, 512
    div bx
    cmp dx, 0   
    jne @a      ; 未除尽,读ax个扇区,因为已经读了1个扇区
    dec ax
@a:
    cmp ax, 0
    je @calc 
    mov cx, app_lab_start + 1
    mov bx, 0
    call read_disk_sector
    ; 程序全部加载置内存
@calc:
    ; 段重定位
    mov dx,[0x08]
    mov ax,[0x06]
    call re_compute
    mov [0x06], ax
    mov cx, [0x0a]  ; 重定位的项目数量
    mov bx, 0x0c    ; 重定位表首地址
realloc:
    mov dx, [bx + 0x02] ; ds已指向用户程序
    mov ax, [bx]
    call re_compute
    mov [bx], ax
    add bx, 4
    loop realloc
    jmp far [0x04]

re_compute:
    push dx
    add ax, [cs:phy_base] ; 即0x10000, ds已经指向程序了
    adc dx, [cs:phy_base + 0x02] ; adc是带进位加法,它将目的操作数和源操作数相加,然后再加上标志寄存器CF位的值(0或者1),分两步就可以完成32位数的加法运算。
    shr ax, 4 ; ax 右移四位,ax低4位一定是0
    ror dx, 4 ; dx 循环右移四位
    and dx, 0xf000 ; DX:AX中是32位的用户程序起始物理内存地址,
                   ; 理论上,它只有20位是有效的,低16位在寄存器AX中,高4位在寄存器DX的低4位。
                   ; 寄存器AX经右移后,高4位已经空出,只要将DX的最低4位挪到这里,就可以得到我们所需要的逻辑段地址。
    or ax, dx 
    pop dx  
    ret

read_disk_sector: ; LBA逻辑扇区号高位存储在bx, 低位存储在cx中, 读取的数据存储在es:di,读取的扇区数存储在al中,最大一次性256个扇区
    push ax ; 保存读取的扇区数,向接口写入信息时,会更改该寄存器,弹出时,弹到ax中
    ; 向磁盘接口写入读写信息
    mov dx, 0x1f2
    out dx, al; 要读取多少个扇区

    mov al, cl
    mov dx, 0x1f3
    out dx, al ; 0x1f3号端口存放的是0~7位

    mov al, ch
    mov dx, 0x1f4
    out dx, al ; 0x1f4号端口存放的是8~15位

    mov al, bl
    mov dx, 0x1f5
    out dx, al ; 0x1f5号端口存放的是16~23位

    mov al, bh
    mov dx, 0x1f6 ; 0x1f6端口的低4位用于存放逻辑扇区号的24~27位,第4位用于指示硬盘号,0表示主盘,1表示从盘。高3位是“111”,表示LBA模式。
    and al, 0b00001111 ; 低四位不变
    or al, 0xe0 ; 高四位置1110
    out dx, al; 0x1f6号端口存放的是24~27位

    ; 写入控制信息, 读
    mov dx, 0x1f7
    mov al, 0x20
    out dx, al

waits:
    in al, dx
    and al, 0x88
    cmp al, 0x08
    jne waits

    pop ax ; 弹出读取的扇区数
    mov bx, 256
    mul bx

    mov cx, ax
    mov dx, 0x1f0 ; 0x1f0是硬盘接口的数据端口,且16位端口。
read:
    in ax, dx
    mov [es:di], ax
    add di, 2
    loop read
    ; 0x1f1端口是错误寄存器
   ret


phy_base dd 0x10000 

end_file:
    times 510-($-$$) db 0
    db 0x55
    db 0xaa

用户程序

  • 用户程序创建三个段,分别为代码段,数据段,栈段。
  • 用户程序头部包含该用户程序长度,用户程序入口点地址,三个栈段的地址的重定位表。这些信息提供给加载器,加载器会重定位栈段信息。
  • 屏幕显示见 NASM(四)显示控制,将其从主引导程序改为用户程序。
section head align=16 vstart=0
    program_length dd program_end

    code_entry dw start               ; 偏移地址
               dd section.code.start  ; 段地址
    
    realloc_tbl_len dw (header_end-code_segment) / 4  ; 每个表项占用4字节
    code_segment dd section.code.start
    data_segment dd section.data.start
    stack_segment dd section.stack.start
header_end:

section code align=16 vstart=0
start:
    mov ax, [data_segment]
    mov ds, ax  ; 设置数据段
    mov ax, 0
    mov si, ax  ; 设置源数据地址
    mov ax, [stack_segment]
    mov ss, ax  ; 设置栈段
    mov sp, stack_end

    mov bx, 0        ; 先将鼠标指针设置为0
    call set_cursor  ;

    mov di, 0   ; 偏移
    mov ax, end_data - data

put:
    mov cl, [ds:si]  ; 拿字符
    add si, 1        ; 偏移+1
    push si          ; 入栈保存
    push ax          ; 入栈保存
    call put_char
    pop ax
    pop si
    dec ax
    cmp ax, 0        ; 判断数据是否传输完
    jne put

exit:
    jmp near exit


get_cursor:
    mov dx, 0x3d4  ; 索引寄存器
    mov al, 0x0e   ; 0x0e光标寄存器
    out dx, al     ; 选择 0x0e光标寄存器, 高8位
    mov dx, 0x3d5  ; 读数据
    in al, dx      ; 读
    mov ah, al     ; 放到高8位

    mov dx, 0x3d4  ; 索引寄存器
    mov al, 0x0f   ; 0x0f光标寄存器
    out dx, al     ; 选择 0x0e光标寄存器, 高8位
    mov dx, 0x3d5  ; 读数据
    in al, dx      ; 读
    ; mov bx, ax     ; 把光标信息存储在bx中
    ret

put_char:          ; 接受参数cl, 用于提供要显示的ASCII码。
    call get_cursor
    cmp cl, 0x0d   ; 判断是否是0x0d
    jne check_0x0a ; 判断是否是0x0a
    mov bl, 80     ; 判断行列
    div bl         ; 被除数存储在"AX"寄存器中 "AL"中得到的是当前行的行号。
                   ; 商(结果的整数部分)将存储在"AL"寄存器中
                   ; 余数(结果的小数部分)将存储在"AH"寄存器中
    mul bl         ; 乘数应存储在AL寄存器中
                   ; 结果的低8位存储在"AL"寄存器中
                   ; 结果的高8位存储在"AH"寄存器中
    mov bx, ax     ; 保存到bx中

    jmp set_cursor

check_0x0a:
    cmp cl, 0x0a
    jne put_c
    add bx, 80      ; 换行
    cmp bx, 2000
    jl set_cursor   ; 屏幕只能显示2000个字符, 多余的滚行
    mov ax,0xb800
    mov ds,ax
    mov es,ax
    cld             ; 清除标志位,使movsw向正方向前进
    mov si,0xa0
    mov di,0x00
    mov cx,1920     ; 位置前挪,空出最后一行
    rep movsw       ; 该指令需要配合源地址寄存器(SI),目标地址寄存器(DI)和计数寄存器(CX)一起使用
                    ; 它会根据CX寄存器中的计数值重复执行movsw指令
clean:              ; 最后一行全部置为空格
    mov bx, 3840    ; 屏幕上第25行第1列在显存中的偏移地址是3840
    mov cx, 80
    mov word[es:bx], 0x0720  ; 清除第25行的内容
    add bx, 2
    loop clean
    mov bx,1920     ; 设置光标

set_cursor:
    mov dx,0x3d4    ; 索引寄存器
    mov al,0x0e     ; 0x0e光标寄存器
    out dx,al       ; 选择0x0e光标寄存器, 高8位
    mov dx,0x3d5    ; 选择0x3d5,准备写
    mov al,bh       ; 准备数据
    out dx,al       ; 写
    mov dx,0x3d4
    mov al,0x0f
    out dx,al
    mov dx,0x3d5
    mov al,bl
    out dx,al
    ret

put_c:
    mov ax,0xb800
    mov es,ax
    shl bx,1         ; bx 乘2
    mov byte [es:bx], cl
    inc bx
    mov byte [es:bx], 0x07  ; 白字黑底
    dec bx
    shr bx,1          ; bx 除2
    add bx,1
    jmp set_cursor


section data vstart=0 align=16
data:
    msg0 db 'This is NASM - the famous Netwide Assembler. ',0x0d,0x0a
         db 'Back at SourceForge and in intensive development! ',0x0d,0x0a
         db 'Get the current versions from http://www.nasm.us/.',0x0d,0x0a
         db 0x0d,0x0a,0x0d,0x0a
         db '  Example code for reverse string:',0x0d,0x0a,0x0d,0x0a
         db '     mov ax, 0x7c0',0x0d,0x0a
         db '     mov ds, ax',0x0d,0x0a
         db '     mov ax, 0x7c00',0x0d,0x0a
         db '     mov ss, ax',0x0d,0x0a
         db '     mov sp, ax',0x0d,0x0a
         db '     mov ax, 0xb800',0x0d,0x0a
         db '     mov es, ax',0x0d,0x0a
         db '     mov cx, 6',0x0d,0x0a
         db '     mov di, data',0x0d,0x0a
         db '     xor bx, bx',0x0d,0x0a
         
end_data:


section stack vstart=0 align=16
    resb 256 ; 声明栈
stack_end:

section end align=16
program_end:

声明:Hello World|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - NASM(五)加载用户程序及用户程序


我的朋友,理论是灰色的,而生活之树是常青的!