《ORANGE-s-一个操作系统实现》内核雏形(3-1)

在Linux下红汇编写Hello World

因为在后面的很多工作中,都要用到汇编编程,所以我们先试试用汇编在Linux下编写Hello World,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
; 编译链接方法
; (ld 的‘-s’选项意为“strip all”)
;
; $ nasm -f elf hello.asm -o hello.o
; $ ld -s hello.o -o hello
; $ ./hello
; Hello, world!
; $

[section .data] ; 数据在此

strHello db "Hello, world!", 0Ah
STRLEN equ $ - strHello

[section .text] ; 代码在此

global _start ; 我们必须导出 _start 这个入口,以便让链接器识别

_start:
mov edx, STRLEN
mov ecx, strHello
mov ebx, 1
mov eax, 4 ; sys_write
int 0x80 ; 系统调用
mov ebx, 0
mov eax, 1 ; sys_exit
int 0x80 ; 系统调用

运行结果如下:
mark
以上代码我们只要明白一点,链接程序只能通过global关键字将默认入口点“_start”导出即可。

汇编和C同步使用

这部分是今后会经常用到的,两者的调用方法如下图:
mark
foo.asm的具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
; 编译链接方法
; (ld 的‘-s’选项意为“strip all”)
;
; $ nasm -f elf foo.asm -o foo.o
; $ gcc -c bar.c -o bar.o
; $ ld -s hello.o bar.o -o foobar
; $ ./foobar
; the 2nd one
; $

extern choose ; int choose(int a, int b);

[section .data] ; 数据在此

num1st dd 3
num2nd dd 4

[section .text] ; 代码在此

global _start ; 我们必须导出 _start 这个入口,以便让链接器识别。
global myprint ; 导出这个函数为了让 bar.c 使用

_start:
push dword [num2nd] ; `.
push dword [num1st] ; |
call choose ; | choose(num1st, num2nd);
add esp, 8 ; /

mov ebx, 0
mov eax, 1 ; sys_exit
int 0x80 ; 系统调用

; void myprint(char* msg, int len)
myprint:
mov edx, [esp + 8] ; len
mov ecx, [esp + 4] ; msg
mov ebx, 1
mov eax, 4 ; sys_write
int 0x80 ; 系统调用
ret

bar.c的具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
void myprint(char* msg, int len);

int choose(int a, int b)
{
if(a >= b){
myprint("the 1st one\n", 13);
}
else{
myprint("the 2nd one\n", 13);
}

return 0;
}

运行结果如图:
mark
从代码中可以看出,其实global和extern两个关键字才是最关键的,可以方便地在汇编和C之间自由变换。

ELF(Executable and Linkable format)

下图为ELF文件的结构由ELF头、程序头表、节和节头表(实际上一个文件不一定包含全部这些内容,而且它们的位置也不一定如下,只有ELF头的位置是固定的,其余部分都是由ELF头中的各项值来决定的)
mark
下面是ELFheader的数据类型:
mark
在终端输入

1
vi /usr/include/elf.h

可以看到:
ELF header的格式
其中各项额意义为:

  • e_ident:其中包含用来表示ELF文件的字符,以及其他一些与机器无关的信息。
    从下图可以很清楚的看出foobar文件的该信息:
    mark
  • e_type:标识文件的类型,其中2代表可执行文件
  • e_machine:表示改程序需要的体系结构,3为Intel80386
  • e_version:文件版本
  • e_entry:程序的入口,这里为0x80480A0
  • e_phoff:文件中的偏移量这里为0x34
  • e_ehsize:大小
  • e_phentsize:每一个条目的大小
  • e_phnum:条目数
  • e_shstrndx:包含节名称的字符串是第几个节。
    从上面的Program header可以看出,foobar在内存中的加载为如下图所示:
    mark

    从Loader到内核

    研究完了ELF之后,我们接着就该完成Loader要做的两件事了
  • 加载内存到内核
  • 跳入保护模式

    用Loader加载ELF

    首先,我们把FAT12文件有关的东西放进fat12hdr.inc中,供boot.asm和loader.asm共享,所以boot.asm开头部分的代码就如下图:
    mark
    下面我们修改loader.asm,让它把内核放进内存,其代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    org  0100h

    BaseOfStack equ 0100h

    BaseOfKernelFile equ 08000h ; KERNEL.BIN 被加载到的位置 ---- 段地址
    OffsetOfKernelFile equ 0h ; KERNEL.BIN 被加载到的位置 ---- 偏移地址


    jmp LABEL_START ; Start

    ; 下面是 FAT12 磁盘的头, 之所以包含它是因为下面用到了磁盘的一些信息
    %include "fat12hdr.inc"


    LABEL_START: ; <--- 从这里开始 *************
    mov ax, cs
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, BaseOfStack

    mov dh, 0 ; "Loading "
    call DispStr ; 显示字符串

    ; 下面在 A 盘的根目录寻找 KERNEL.BIN
    mov word [wSectorNo], SectorNoOfRootDirectory
    xor ah, ah ; `.
    xor dl, dl ; | 软驱复位
    int 13h ; /
    LABEL_SEARCH_IN_ROOT_DIR_BEGIN:
    cmp word [wRootDirSizeForLoop], 0 ; `.
    jz LABEL_NO_KERNELBIN ; | 判断根目录区是不是已经读完,
    dec word [wRootDirSizeForLoop] ; / 读完表示没有找到 KERNEL.BIN
    mov ax, BaseOfKernelFile
    mov es, ax ; es <- BaseOfKernelFile
    mov bx, OffsetOfKernelFile ; bx <- OffsetOfKernelFile
    mov ax, [wSectorNo] ; ax <- Root Directory 中的某 Sector 号
    mov cl, 1
    call ReadSector

    mov si, KernelFileName ; ds:si -> "KERNEL BIN"
    mov di, OffsetOfKernelFile
    cld
    mov dx, 10h
    LABEL_SEARCH_FOR_KERNELBIN:
    cmp dx, 0 ; `.
    jz LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR; | 循环次数控制, 如果已经读完
    dec dx ; / 了一个 Sector, 就跳到下一个
    mov cx, 11
    LABEL_CMP_FILENAME:
    cmp cx, 0 ; `.
    jz LABEL_FILENAME_FOUND ; | 循环次数控制, 如果比较了 11 个字符都
    dec cx ; / 相等, 表示找到
    lodsb ; ds:si -> al
    cmp al, byte [es:di] ; if al == es:di
    jz LABEL_GO_ON
    jmp LABEL_DIFFERENT
    LABEL_GO_ON:
    inc di
    jmp LABEL_CMP_FILENAME ; 继续循环

    LABEL_DIFFERENT:
    and di, 0FFE0h ; else`. 让 di 是 20h 的倍数
    add di, 20h ; |
    mov si, KernelFileName ; | di += 20h 下一个目录条目
    jmp LABEL_SEARCH_FOR_KERNELBIN; /

    LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR:
    add word [wSectorNo], 1
    jmp LABEL_SEARCH_IN_ROOT_DIR_BEGIN

    LABEL_NO_KERNELBIN:
    mov dh, 2 ; "No KERNEL."
    call DispStr ; 显示字符串
    %ifdef _LOADER_DEBUG_
    mov ax, 4c00h ; `.
    int 21h ; / 没有找到 KERNEL.BIN, 回到 DOS
    %else
    jmp $ ; 没有找到 KERNEL.BIN, 死循环在这里
    %endif

    LABEL_FILENAME_FOUND: ; 找到 KERNEL.BIN 后便来到这里继续
    mov ax, RootDirSectors
    and di, 0FFF0h ; di -> 当前条目的开始

    push eax
    mov eax, [es : di + 01Ch] ; `.
    mov dword [dwKernelSize], eax ; / 保存 KERNEL.BIN 文件大小
    pop eax

    add di, 01Ah ; di -> 首 Sector
    mov cx, word [es:di]
    push cx ; 保存此 Sector 在 FAT 中的序号
    add cx, ax
    add cx, DeltaSectorNo ; cl <- LOADER.BIN 的起始扇区号(0-based)
    mov ax, BaseOfKernelFile
    mov es, ax ; es <- BaseOfKernelFile
    mov bx, OffsetOfKernelFile ; bx <- OffsetOfKernelFile
    mov ax, cx ; ax <- Sector 号

    LABEL_GOON_LOADING_FILE:
    push ax ; `.
    push bx ; |
    mov ah, 0Eh ; | 每读一个扇区就在 "Loading " 后面
    mov al, '.' ; | 打一个点, 形成这样的效果:
    mov bl, 0Fh ; | Loading ......
    int 10h ; |
    pop bx ; |
    pop ax ; /

    mov cl, 1
    call ReadSector
    pop ax ; 取出此 Sector 在 FAT 中的序号
    call GetFATEntry
    cmp ax, 0FFFh
    jz LABEL_FILE_LOADED
    push ax ; 保存 Sector 在 FAT 中的序号
    mov dx, RootDirSectors
    add ax, dx
    add ax, DeltaSectorNo
    add bx, [BPB_BytsPerSec]
    jmp LABEL_GOON_LOADING_FILE
    LABEL_FILE_LOADED:

    call KillMotor ; 关闭软驱马达

    mov dh, 1 ; "Ready."
    call DispStr ; 显示字符串

    jmp $


    ;============================================================================
    ;变量
    ;----------------------------------------------------------------------------
    wRootDirSizeForLoop dw RootDirSectors ; Root Directory 占用的扇区数
    wSectorNo dw 0 ; 要读取的扇区号
    bOdd db 0 ; 奇数还是偶数
    dwKernelSize dd 0 ; KERNEL.BIN 文件大小

    ;============================================================================
    ;字符串
    ;----------------------------------------------------------------------------
    KernelFileName db "KERNEL BIN", 0 ; KERNEL.BIN 之文件名
    ; 为简化代码, 下面每个字符串的长度均为 MessageLength
    MessageLength equ 9
    LoadMessage: db "Loading "
    Message1 db "Ready. "
    Message2 db "No KERNEL"
    ;============================================================================

    ;----------------------------------------------------------------------------
    ; 函数名: DispStr
    ;----------------------------------------------------------------------------
    ; 作用:
    ; 显示一个字符串, 函数开始时 dh 中应该是字符串序号(0-based)
    DispStr:
    mov ax, MessageLength
    mul dh
    add ax, LoadMessage
    mov bp, ax ; ┓
    mov ax, ds ; ┣ ES:BP = 串地址
    mov es, ax ; ┛
    mov cx, MessageLength ; CX = 串长度
    mov ax, 01301h ; AH = 13, AL = 01h
    mov bx, 0007h ; 页号为0(BH = 0) 黑底白字(BL = 07h)
    mov dl, 0
    add dh, 3 ; 从第 3 行往下显示
    int 10h ; int 10h
    ret
    ;----------------------------------------------------------------------------
    ; 函数名: ReadSector
    ;----------------------------------------------------------------------------
    ; 作用:
    ; 从序号(Directory Entry 中的 Sector 号)为 ax 的的 Sector 开始, 将 cl 个 Sector 读入 es:bx 中
    ReadSector:
    ; -----------------------------------------------------------------------
    ; 怎样由扇区号求扇区在磁盘中的位置 (扇区号 -> 柱面号, 起始扇区, 磁头号)
    ; -----------------------------------------------------------------------
    ; 设扇区号为 x
    ; ┌ 柱面号 = y >> 1
    ; x ┌ 商 y ┤
    ; -------------- => ┤ └ 磁头号 = y & 1
    ; 每磁道扇区数 │
    ; └ 余 z => 起始扇区号 = z + 1
    push bp
    mov bp, sp
    sub esp, 2 ; 辟出两个字节的堆栈区域保存要读的扇区数: byte [bp-2]

    mov byte [bp-2], cl
    push bx ; 保存 bx
    mov bl, [BPB_SecPerTrk] ; bl: 除数
    div bl ; y 在 al 中, z 在 ah 中
    inc ah ; z ++
    mov cl, ah ; cl <- 起始扇区号
    mov dh, al ; dh <- y
    shr al, 1 ; y >> 1 (其实是 y/BPB_NumHeads, 这里BPB_NumHeads=2)
    mov ch, al ; ch <- 柱面号
    and dh, 1 ; dh & 1 = 磁头号
    pop bx ; 恢复 bx
    ; 至此, "柱面号, 起始扇区, 磁头号" 全部得到 ^^^^^^^^^^^^^^^^^^^^^^^^
    mov dl, [BS_DrvNum] ; 驱动器号 (0 表示 A 盘)
    .GoOnReading:
    mov ah, 2 ; 读
    mov al, byte [bp-2] ; 读 al 个扇区
    int 13h
    jc .GoOnReading ; 如果读取错误 CF 会被置为 1, 这时就不停地读, 直到正确为止

    add esp, 2
    pop bp

    ret

    ;----------------------------------------------------------------------------
    ; 函数名: GetFATEntry
    ;----------------------------------------------------------------------------
    ; 作用:
    ; 找到序号为 ax 的 Sector 在 FAT 中的条目, 结果放在 ax 中
    ; 需要注意的是, 中间需要读 FAT 的扇区到 es:bx 处, 所以函数一开始保存了 es 和 bx
    GetFATEntry:
    push es
    push bx
    push ax
    mov ax, BaseOfKernelFile ; ┓
    sub ax, 0100h ; ┣ 在 BaseOfKernelFile 后面留出 4K 空间用于存放 FAT
    mov es, ax ; ┛
    pop ax
    mov byte [bOdd], 0
    mov bx, 3
    mul bx ; dx:ax = ax * 3
    mov bx, 2
    div bx ; dx:ax / 2 ==> ax <- 商, dx <- 余数
    cmp dx, 0
    jz LABEL_EVEN
    mov byte [bOdd], 1
    LABEL_EVEN:;偶数
    xor dx, dx ; 现在 ax 中是 FATEntry 在 FAT 中的偏移量. 下面来计算 FATEntry 在哪个扇区中(FAT占用不止一个扇区)
    mov bx, [BPB_BytsPerSec]
    div bx ; dx:ax / BPB_BytsPerSec ==> ax <- 商 (FATEntry 所在的扇区相对于 FAT 来说的扇区号)
    ; dx <- 余数 (FATEntry 在扇区内的偏移)。
    push dx
    mov bx, 0 ; bx <- 0 于是, es:bx = (BaseOfKernelFile - 100):00 = (BaseOfKernelFile - 100) * 10h
    add ax, SectorNoOfFAT1 ; 此句执行之后的 ax 就是 FATEntry 所在的扇区号
    mov cl, 2
    call ReadSector ; 读取 FATEntry 所在的扇区, 一次读两个, 避免在边界发生错误, 因为一个 FATEntry 可能跨越两个扇区
    pop dx
    add bx, dx
    mov ax, [es:bx]
    cmp byte [bOdd], 1
    jnz LABEL_EVEN_2
    shr ax, 4
    LABEL_EVEN_2:
    and ax, 0FFFh

    LABEL_GET_FAT_ENRY_OK:

    pop bx
    pop es
    ret
    ;----------------------------------------------------------------------------


    ;----------------------------------------------------------------------------
    ; 函数名: KillMotor
    ;----------------------------------------------------------------------------
    ; 作用:
    ; 关闭软驱马达
    KillMotor:
    push dx
    mov dx, 03F2h
    mov al, 0
    out dx, al
    pop dx
    ret
    ;----------------------------------------------------------------------------

下面接着就是写一个内核出来,文件名为kernel.asm

1
2
3
4
5
6
7
8
9
10
11
12
13
; 编译链接方法
; $ nasm -f elf kernel.asm -o kernel.o
; $ ld -s kernel.o -o kernel.bin #‘-s’选项意为“strip all”

[section .text] ; 代码在此

global _start ; 导出 _start

_start: ; 跳到这里来的时候,我们假设 gs 指向显存
mov ah, 0Fh ; 0000: 黑底 1111: 白字
mov al, 'K'
mov [gs:((80 * 1 + 39) * 2)], ax ; 屏幕第 1 行, 第 39 列。
jmp $

显示字符时涉及内存操作,所以用到GDT,我们假设在Loader中段寄存器gs已经指向显存的开始。
运行结果如下:
mark

您的支持将鼓励我继续创作!