0%

操作系统真相还原Ch6-1

完善内核

函数调用约定

将函数参数放入栈中,栈放入内存中好处:

  • 每个进程都有自己的栈
  • 保存参数的内存地址不用再花精力维护,已经有栈机制来维护地址变化,参数在栈中的位置可以通过栈顶的偏移量得到
两个调用约定stdcall、cdecl
  • stdcall
    • 调用者将所有参数从右向左入栈
    • 被调用者清理参数所占的栈空间
  • cdecl
    • 调用者将所有的参数从右向左入栈
    • 调用者清理参数所占的栈空间

cdecl允许函数中的参数数量不固定

汇编语言和C语言混合编程

汇编语言与C语言混合编程可以分为两类

  • 单独的汇编代码文件与单独的C语言文件分别编译成目标文件,一起链接成可执行程序
  • 在C语言中嵌入汇编代码,直接编译生成可执行程序(又称为内联汇编)

系统调用时Linux内核提供的一套子程序,用来实现一系列在用户态不能或不易实现的功能;系统调用的入口只有一个,即第0x80号中断

系统调用两种方式:

  • 将系统调用指令封装为c库函数,通过库函数进行系统调用
  • 不依赖任何库函数,直接通过汇编指令int与操作系统通信

当输入参数小于等于5个时,Linux使用寄存器传递参数,当参数大于5个时,把参数按照顺序放入连续的内存区域,并将该区域的首地址放入ebx寄存器。ebx存储第1个参数,ecx存储第2个参数,edx存储第3个参数,esi存储第4个参数,edi存储第5个参数。

函数声明的作用:
  • 告诉编译器该函数的参数所需要的栈空间大小以及返回值
  • 如果该函数是在外部定义的,一定要在链接阶段将其对应的目标文件一块链接进来

实现自己的打印函数

显卡的端口控制

VGA寄存器

端口就是IO接口电路上的寄存器,用专门的IO指令in、out来读写寄存器。他们把每一个寄存器分组视为一个寄存器数组,提供一个寄存器用于指定数组下标,再提供一个寄存器用于对索引所指向的数组元素(也就是寄存器)进行输入输出操作。这样用这两个寄存器就能够定位寄存器数组中的任何寄存器啦。Address Register作为数组索引,Data Register作为寄存器数组中该索引对应的寄存器。设置数组是因为如果一个寄存器对应一个端口(有限)会浪费资源,所以用数组来使得一个端口对应多个寄存器。

新建了lib目录专门存放库文件,在lib下有user和kernel两个子目录,供内核使用的库文件放在lib/kernel里面,用户进程使用的库文件放在lib/user里面

/lib/stdint.h

1
2
3
4
5
6
7
8
9
10
11
#ifndef __LIB_STDINT_H
#define __LIB_STDINT_H
typedef signed char int8_t;
typedef signed short int int16_t;
typedef signed int int32_t;
typedef signed long long int int64_t;
typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long long int uint64_t;
#endif

/lib/kernel/print.S

处理流程:

(1)备份寄存器现场。
(2)获取光标坐标值,光标坐标值是下一个可打印字符的位置。
(3)获取待打印的字符。
(4)判断字符是否为控制字符,若是回车符、换行符、退格符三种控制字符之一,则进入相应的处理流程。否则,其余字符都被粗暴地认为是可见字符,进入输出流程处理。
(5)判断是否需要滚屏。
(6)更新光标坐标值,使其指向下一个打印字符的位置。
(7)恢复寄存器现场,退出。

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
TI_GDT equ  0
RPL0 equ 0
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0

section .data
put_int_buffer dq 0 ; 定义8字节缓冲区用于数字到字符的转换

[bits 32]
section .text
;--------------------------------------------
;put_str 通过put_char来打印以0字符结尾的字符串
;--------------------------------------------
;输入:栈中参数为打印的字符串
;输出:无

global put_str
put_str:
;由于本函数中只用到了ebx和ecx,只备份这两个寄存器
push ebx
push ecx
xor ecx, ecx ; 准备用ecx存储参数,清空
mov ebx, [esp + 12] ; 从栈中得到待打印的字符串地址
.goon:
mov cl, [ebx]
cmp cl, 0 ; 如果处理到了字符串尾,跳到结束处返回
jz .str_over
push ecx ; 为put_char函数传递参数
call put_char
add esp, 4 ; 回收参数所占的栈空间
inc ebx ; 使ebx指向下一个字符
jmp .goon
.str_over:
pop ecx
pop ebx
ret

;------------------------ put_char -----------------------------
;功能描述:把栈中的1个字符写入光标所在处
;-------------------------------------------------------------------
global put_char
put_char:
pushad ;备份32位寄存器环境
;需要保证gs中为正确的视频段选择子,为保险起见,每次打印时都为gs赋值
mov ax, SELECTOR_VIDEO ; 不能直接把立即数送入段寄存器
mov gs, ax

;;;;;;;;; 获取当前光标位置 ;;;;;;;;;
;先获得高8位
mov dx, 0x03d4 ;索引寄存器,Address Register
mov al, 0x0e ;用于提供光标位置的高8位
out dx, al
mov dx, 0x03d5 ;通过读写数据端口0x3d5来获得或设置光标位置,Data Register
in al, dx ;得到了光标位置的高8位
mov ah, al

;再获取低8位
mov dx, 0x03d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
in al, dx

;将光标存入bx
mov bx, ax
;下面这行是在栈中获取待打印的字符
mov ecx, [esp + 36] ;pushad压入4×8=32字节,加上主调函数的返回地址4字节,故esp+36字节
cmp cl, 0xd ;CR是0x0d,LF是0x0a
jz .is_carriage_return
cmp cl, 0xa
jz .is_line_feed

cmp cl, 0x8 ;BS(backspace)的asc码是8
jz .is_backspace
jmp .put_other
;;;;;;;;;;;;;;;;;;

.is_backspace:
;;;;;;;;;;;; backspace的一点说明 ;;;;;;;;;;
; 当为backspace时,本质上只要将光标移向前一个显存位置即可.后面再输入的字符自然会覆盖此处的字符
; 但有可能在键入backspace后并不再键入新的字符,这时在光标已经向前移动到待删除的字符位置,但字符还在原处,
; 这就显得好怪异,所以此处添加了空格或空字符0
dec bx
shl bx,1
mov byte [gs:bx], 0x20 ;将待删除的字节补为0或空格皆可
inc bx
mov byte [gs:bx], 0x07
shr bx,1
jmp .set_cursor
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

.put_other:
shl bx, 1 ; 光标位置是用2字节表示,将光标值乘2,表示对应显存中的偏移字节
mov [gs:bx], cl ; ascii字符本身
inc bx
mov byte [gs:bx],0x07 ; 字符属性
shr bx, 1 ; 恢复老的光标值
inc bx ; 下一个光标值
cmp bx, 2000
jl .set_cursor ; 若光标值小于2000,表示未写到显存的最后,则去设置新的光标值
; 若超出屏幕字符数大小(2000)则换行处理
.is_line_feed: ; 是换行符LF(\n)
.is_carriage_return: ; 是回车符CR(\r)
; 如果是CR(\r),只要把光标移到行首就行了。
xor dx, dx ; dx是被除数的高16位,清0.
mov ax, bx ; ax是被除数的低16位.
mov si, 80 ; 由于是效仿linux,linux中\n便表示下一行的行首,所以本系统中,
div si ; 把\n和\r都处理为linux中\n的意思,也就是下一行的行首。
sub bx, dx ; 光标值减去除80的余数便是取整
; 以上4行处理\r的代码

.is_carriage_return_end: ; 回车符CR处理结束
add bx, 80
cmp bx, 2000
.is_line_feed_end: ; 若是LF(\n),将光标移+80便可。
jl .set_cursor

;屏幕行范围是0~24,滚屏的原理是将屏幕的1~24行搬运到0~23行,再将第24行用空格填充
.roll_screen: ; 若超出屏幕大小,开始滚屏
cld
mov ecx, 960 ; 一共有2000-80=1920个字符要搬运,共1920*2=3840字节.一次搬4字节,共3840/4=960次
mov esi, 0xb80a0 ; 第1行行首
mov edi, 0xb8000 ; 第0行行首
rep movsd

;;;;;;;将最后一行填充为空白
mov ebx, 3840 ; 最后一行首字符的第一个字节偏移= 1920 * 2
mov ecx, 80 ;一行是80字符(160字节),每次清空1字符(2字节),一行需要移动80次
.cls:
mov word [gs:ebx], 0x0720 ;0x0720是黑底白字的空格键
add ebx, 2
loop .cls
mov bx,1920 ;将光标值重置为1920,最后一行的首字符.

.set_cursor:
;将光标设为bx值
;;;;;;; 1 先设置高8位 ;;;;;;;;
mov dx, 0x03d4 ;索引寄存器
mov al, 0x0e ;用于提供光标位置的高8位
out dx, al
mov dx, 0x03d5 ;通过读写数据端口0x3d5来获得或设置光标位置
mov al, bh
out dx, al

;;;;;;; 2 再设置低8位 ;;;;;;;;;
mov dx, 0x03d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
mov al, bl
out dx, al
.put_char_done:
popad
ret

;-------------------- 将小端字节序的数字变成对应的ascii后,倒置 -----------------------
;输入:栈中参数为待打印的数字
;输出:在屏幕上打印16进制数字,并不会打印前缀0x,如打印10进制15时,只会直接打印f,不会是0xf
;------------------------------------------------------------------------------------------
global put_int
put_int:
pushad
mov ebp, esp
mov eax, [ebp+4*9] ; call的返回地址占4字节+pushad的8个4字节
mov edx, eax
mov edi, 7 ; 指定在put_int_buffer中初始的偏移量
mov ecx, 8 ; 32位数字中,16进制数字的位数是8个
mov ebx, put_int_buffer

;将32位数字按照16进制的形式从低位到高位逐个处理,共处理8个16进制数字
.16based_4bits: ; 每4位二进制是16进制数字的1位,遍历每一位16进制数字
and edx, 0x0000000F ; 解析16进制数字的每一位。and与操作后,edx只有低4位有效
cmp edx, 9 ; 数字0~9和a~f需要分别处理成对应的字符
jg .is_A2F
add edx, '0' ; ascii码是8位大小。add求和操作后,edx低8位有效。
jmp .store
.is_A2F:
sub edx, 10 ; A~F 减去10 所得到的差,再加上字符A的ascii码,便是A~F对应的ascii码
add edx, 'A'

;将每一位数字转换成对应的字符后,按照类似“大端”的顺序存储到缓冲区put_int_buffer
;高位字符放在低地址,低位字符要放在高地址,这样和大端字节序类似,只不过咱们这里是字符序.
.store:
; 此时dl中应该是数字对应的字符的ascii码
mov [ebx+edi], dl
dec edi
shr eax, 4
mov edx, eax
loop .16based_4bits

;现在put_int_buffer中已全是字符,打印之前,
;把高位连续的字符去掉,比如把字符000123变成123
.ready_to_print:
inc edi ; 此时edi退减为-1(0xffffffff),加1使其为0
.skip_prefix_0:
cmp edi,8 ; 若已经比较第9个字符了,表示待打印的字符串为全0
je .full0
;找出连续的0字符, edi做为非0的最高位字符的偏移
.go_on_skip:
mov cl, [put_int_buffer+edi]
inc edi
cmp cl, '0'
je .skip_prefix_0 ; 继续判断下一位字符是否为字符0(不是数字0)
dec edi ;edi在上面的inc操作中指向了下一个字符,若当前字符不为'0',要恢复edi指向当前字符
jmp .put_each_num

.full0:
mov cl,'0' ; 输入的数字为全0时,则只打印0
.put_each_num:
push ecx ; 此时cl中为可打印的字符
call put_char
add esp, 4
inc edi ; 使edi指向下一个字符
mov cl, [put_int_buffer+edi] ; 获取下一个字符到cl寄存器
cmp edi,8
jl .put_each_num
popad
ret

需要滚屏的情况:

  • 新的光标值超出了屏幕右下角最后一个字符的位置
  • 最后一行中任意位置有回车或换行符

屏幕每行80个字符,共25行,实现滚屏的步骤:

  • 将1-24行的内容搬到0-23行
  • 再将第24行即最后一行用空格覆盖,看上去是一个新的空行
  • 把光标移到第24行也就是最后一行行首

几个术语:

  • CPL:当前特权级,也就是程序运行时所处的特权级
  • RPL:请求特权级,是选择子中的RPL字段
  • DPL:段描述符所代表的内存区域的特权级

CPU在执行iretd指令时会做特权检查,它检查DS、ES、FS、GS“数据”段寄存器的内容,它只是修饰段寄存器的定语,用来指除代码段寄存器CS和栈段寄存器SS之外的段寄存器。在32位保护模式下它们中存储的都是选择子,要是有任何一个段寄存器所指向的段描述符的DPL权限高于从iretd命令返回后的CPL(这个返回后的CPL也就是新的CPL,CPL就是加载到CS寄存器中选择子的RPL),CPU就会将该段寄存器赋值为0

用户进程的特权级由CS寄存器中选择子RPL字段决定,它将成为进程在CPU上运行的CPL。

lib/kernel/print.h

1
2
3
4
5
6
7
#ifndef __LIB_KERNEL_PRINT_H
#define __LIB_KERNEL_PRINT_H
#include "stdint.h"
void put_char(uint8_t char_asci);
void put_str(char* message);
void put_int(uint32_t num); // 以16进制打印
#endif
1
2
3
4
5
6
7
8
9
nasm -I include/ -o mbr.bin mbr.S && \
nasm -I include/ -o loader.bin loader.S && \
nasm -f elf -o ./lib/kernel/print.o ./lib/kernel/print.S && \
gcc -m32 -I ./lib/kernel/ -c -o ./kernel/main.o ./kernel/main.c && \
ld ./kernel/main.o ./lib/kernel/print.o -N -Ttext=0xc0001500 -e main -m elf_i386 -o ./kernel/kernel.bin && \
dd if=/root/bochs-2.7/mbr.bin of=/root/bochs-2.7/hd60M.img bs=512 count=1 conv=notrunc && \
dd if=/root/bochs-2.7/loader.bin of=/root/bochs-2.7/hd60M.img bs=512 count=4 seek=2 conv=notrunc && \
dd if=/root/bochs-2.7/kernel/kernel.bin of=/root/bochs-2.7/hd60M.img bs=512 count=200 seek=9 conv=notrunc && \
bochs -f bochsrc.disk

注:main.o在前,print.o在后,即调用在前、实现在后,如果反过来则会导致生成的虚拟地址不准确。