《操作系统真象还原》第6章

函数调用过程(cdecl调用约定):

以下面这段代码为例:


步骤:

0.转移文件

1.浅析C库函数与系统调用

2.实现打印函数


0.转移文件

感觉将这么多与系统启动有关的文件放在bochs下确实不太好看,于是决定mkdir boot,将以下四个文件放进去:

于是bochs下的文件还剩下:

有没有感觉好点了。


1.浅析C库函数与系统调用

创建syscall_write.S文件:

复制代码
 1 section .data
 2 str_c_lib: db "c library says: hello world!", 0xa ;0xa为LF ASCII码
 3 str_c_lib_len equ $-str_c_lib
 4 
 5 str_syscall: db "syscall says: hello world!", 0xa
 6 str_syscall_len equ $-str_syscall
 7 
 8 section .text
 9 global _start
10 _start:
11    ;---------- 方式1:模拟C语言中系统调用库函数wite -----------
12    push str_c_lib_len   ;按照C调用约定压入参数
13    push str_c_lib
14    push 1
15    
16    call simu_write      ;调用下面定于的simu_write
17    add esp,12           ;回收栈空间
18    
19    ;---------- 方式2:跨过库函数,直接进行系统调用 ------------
20    mov eax,4            ;4号子功能是write系统调用
21    mov ebx,1
22    mov ecx,str_syscall
23    mov edx,str_syscall_len
24    int 0x80             ;发起中断,通知linux完成请求的功能
25   
26    ;----------------------- 退出程序 --------------------------
27    mov eax,1            ;1号子功能是exit
28    int 0x80             ;发起中断,通知linux完成请求的功能
29 
30    ;----- 下面自定义的simu_write用来模拟C库中系统调用write ----
31 simu_write:
32    push ebp
33    mov ebp,esp
34    mov eax,4
35    mov ebx,[ebp+8]
36    mov ecx,[ebp+12]
37    mov edx,[ebp+16]
38    int 0x80
39    pop ebp
40    ret
复制代码

编译链接执行:

nasm -f elf -o syscall_write.o syscall_write.S
ld -m elf_i386 -o syscall_write.bin syscall_write.o
./syscall_write.bin

执行结果:

当然,干货在后面,这个文件就是试试水,它已经没啥用了,删掉吧。


 2.实现打印函数

同书中所示,先新建个lib目录用来专门存放各种库文件。不仅如此,在lib目录下还建立了user和kernel两个子目录,以后供内核使用的库文件就放在lib/kernel下,lib/user/中是用户进程使用的库文件。

为了开发方便,我们定义一些数据类型。主要是参考Linux的/usr/include/stdint.h文件,在bochs下vim lib/stdint.h:

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

然后用汇编语言(没错,它还在)实现打印字符函数put_char,在lib/kernel/print.S文件中完成:

复制代码
  1 TI_GDT equ 0
  2 RPL0   equ 0
  3 SELECTOR_VIDEO equ (0x0003<<3)+TI_GDT+RPL0
  4 
  5 section .data
  6 put_int_buffer dq 0        ;定义8字节缓冲区用于数字到字符的转换
  7 
  8 [bits 32]
  9 section .text
 10    ;-------------------- put_char ---------------------
 11    ;功能描述:把栈中的1个字符写入光标所在处
 12    ;---------------------------------------------------
 13 global put_char:
 14 put_char:
 15    pushad                  ;备份32位寄存器环境,需要保证gs为正确的视频段选择子,保>险起见,每次打印时都为gs赋值
 16    mov ax,SELECTOR_VIDEO
 17    mov gs,ax
 18 
 19    ;获取当前光标位置
 20    ;先获取高8位
 21    mov dx,0x03d4
 22    mov al,0x0e
 23    out dx,al
 24    mov dx,0x03d5
 25    in al,dx
 26    mov ah,al
 27    ;再获取低8位
 28    mov dx,0x03d4
 29    mov al,0x0f
 30    out dx,al
 31    mov dx,0x03d5
 32    in al,dx
 33 
 34    ;将光标存入bx
 35    mov bx,ax
 36    mov ecx,[esp+36]        ;pushad压入4*8=32B,加上主调函数4B的返回地址,故esp+36B
 37    cmp cl,0xd              ;CR是0x0d,LF是0x0a
 38    jz .is_carriage_return
 39    cmp cl,0xa
 40    jz .is_line_feed
 41 
 42    cmp cl,0x8              ;BS(backspace)的ASCII码是8
 43    jz .is_backspace
 44    jmp .put_other
 45 
 46 .is_backspace:
 47    dec bx
 48    shl bx,1                ;光标左移1位等于乘2,表示光标对应显存中的偏移字节
 49    mov byte [gs:bx],0x20   ;将待删除的字节补为0或空格皆可
 50    inc bx
 51    mov byte [gs:bx],0x07
 52    shr bx,1
 53    jmp .set_cursor
 54 
 55 .put_other:
 56    shl bx,1                ;光标位置用2字节表示,将光标值*2表示对应显存中的偏移字节
 57    mov [gs:bx],cl          ;ASCII字符本身
 58    inc bx
 59    mov byte [gs:bx],0x07   ;字符属性
 60    shr bx,1                ;恢复老光标值
 61    inc bx                  ;下一个光标值
 62    cmp bx,2000
 63    jl .set_cursor          ;若光标值小于2000,表示未写到显存的最后,则去设置新的光>标值,若超出屏幕字符大小(2000),则换行处理
 64 
 65 .is_line_feed:             ;换行符LF(\n)
 66 .is_carriage_return:       ;回车符CR(\r)
 67    ;如果是CR(\r),只要把光标移到行首就行了
 68    xor dx,dx               ;dx是被除数的高16位,清0
 69    mov ax,bx               ;ax是被除数的低16位
 70    mov si,80
 71    div si
 72    sub bx,dx               ;光标值取整
 73 
 74 .is_carriage_return_end:
 75    add bx,80
 76    cmp bx,2000
 77 .is_line_feed_end:         ;若是LF(\n),将光标移+80即可
 78    jl .set_cursor
 79 
 80    ;屏幕行范围是0~24,滚屏的原理是将屏幕第1~24行搬运到第0~23行,再将第23行用空格填>充
 81 .roll_screen:              ;若超出屏幕大小,滚屏
 82    cld
 83    mov ecx,960             ;2000-80=1920个字符,共1920*2=3840B,一次搬4B,共3840/4=960次
 84    mov esi,0xc00b80a0      ;第1行行首
 85    mov edi,0xc00b8000      ;第0行行首
 86    rep movsd
 87 
 88    ;将最后一行填充为空白
 89    mov ebx,3840
 90    mov ecx,80
 91 
 92 .cls:
 93    mov word [gs:ebx],0x0720;0x0720是黑底白字的空格键
 94    add ebx,2
 95    loop .cls
 96    mov bx,1920             ;将光标值重置为1920,最后一行首字符
 97 
 98 .set_cursor:
 99    ;将光标设为bx值
100    ;1.先设置高8位
101    mov dx,0x03d4           ;索引寄存器
102    mov al,0x0e             ;光标位置高8位
103    out dx,al
104    mov dx,0x03d5           ;通过读写数据端口0x3d5来获得或设置光标位置
105    mov al,bh
106    out dx,al
107 
108    ;2.再设置低8位
109    mov dx,0x03d4
110    mov al,0x0f
111    out dx,al
112    mov dx,0x03d5
113    mov al,bl
114    out dx,al
115 .put_char_done:
116    popad
117    ret
复制代码

 再将put_char()函数写在头文件lib/kernel/print.h中,谁需要它就将其包含进来就行了:

1 #ifndef __LIB_KERNEL_PRINT_H
2 #define __LIB_KERNEL_PRINT_H
3 #include "stdint.h"
4 void put_char(uint8_t char_asci);
5 #

下面改进main.c(还记得嘛,它在bochs/kernel/下),在其中用put_char函数打印字符:

复制代码
 1 #include "print.h"
 2 void main(void)
 3 {    
 4   put_char('T');
 5   put_char('h');
 6   put_char('i');
 7   put_char('s');
 8   put_char(' ');
 9   put_char('.');
10   put_char('\b');
11   put_char('i');
12   put_char('s');
13   put_char('\n');
14   put_char('k');
15   put_char('e');
16   put_char('r');
17   put_char('n');
18   put_char('e');
19   put_char('l');
20   put_char('!');
21   while(1);
22 }
复制代码

然后在bochs下编译链接:

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 -m elf_i386 -Ttext 0xc0001500 -e main -o kernel/kernel.bin kernel/main.o lib/kernel/print.o
dd if=/home/zbb/bochs/kernel/kernel.bin of=/home/zbb/bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc
bin/bochs -f bochsrc.disk

如果你在编译时显示:

In file included from lib/kernel/print.h:3,
from kernel/main.c:1: /usr/include/stdint.h:26: fatal error: bits/libc-header-start.h: No such file or directory compilation terminated.

那你可以前往下面这个链接寻找解决方案:

fatal error: bits/libc-header-start.h: 没有那个文件或目录_BUFFER.pwn的博客-CSDN博客

成功后:

没问题,正确显示了“This is\nkernel!”


刚才是打印字符char,接下来尝试打印字符串str。

修改print.S,在之前的put_char后添加:

复制代码
 1    ;-- 功能描述:通过put_char来打印以0字符结尾的字符串 --
 2 global put_str:
 3 put_str:
 4    push ebx
 5    push ecx
 6    xor ecx,ecx
 7    mov ebx,[esp+12]
 8 .put_char_loop:
 9    mov cl,[ebx]
10    cmp cl,0
11    jz .str_over
12    push ecx
13    call put_char
14    add esp,4
15    inc ebx
16    jmp .put_char_loop
17 .str_over:
18    pop ecx
19    pop ebx
20    ret
复制代码

修改print.h

1 #ifndef __LIB_KERNEL_PRINT_H
2 #define __LIB_KERNEL_PRINT_H
3 #include "stdint.h"
4 void put_char(uint8_t char_asci);
5 void put_str(char* message);
6 #endif

修改main.c,在其中任意位置添加一句:

put_str("Welcome,\nI am kernel!");

来看结果:

 差点忘记了测试滚屏功能,来看看:


前面的字符(串)都是带引号的,如字符‘9’,最后再让我们添加一个打印整数的功能,比如数字9:

添加步骤一致,先是print.S

复制代码
 1 global put_int
 2 put_int:
 3    pushad
 4    mov ebp,esp
 5    mov eax,[esp+36]        ;32B+4B返回地址
 6    mov edx,eax
 7    mov edi,7               ;put_int_buffer偏移量
 8    mov ecx,8               ;循环八次
 9    mov ebx,put_int_buffer
10 
11 .16based_4bits:
12    and edx,0x0000000F
13    cmp edx,9
14    jg  .is_A2F             ;A~F的ASCII码
15    add edx,'0'             ;0~9的ASCII码
16    jmp .store
17 
18 .is_A2F:
19    sub edx,10
20    add edx,'A'             ;减去10等于A~F的字符序+'A'得ASCII码
21 .store:
22    mov [ebx+edi],dl        ;此时dl中是数字对应字符的ASCII码
23    dec edi
24    shr eax,4
25    mov edx,eax
26    loop .16based_4bits
27 
28 .ready_to_print:
29    inc edi                 ;edi减退为-1
30 .skip_prefix_0:            ;跳过前导0
31    cmp edi,8               ;edi偏移量为8的时候表示到了第9个字符
32    je .full0               ;前导0有8个,说明全是0
33 
34 .go_on_skip:
35    mov cl,[put_int_buffer+edi]
36    inc edi                 ;下一个字符
37    cmp cl,'0'
38    je .skip_prefix_0       ;判断下一个字符是否为'\0'
39    dec edi                 ;不是'\0',edi减1恢复当前字符
40    jmp .put_each_num
41 
42 .full0:
43    mov cl,'0'              ;全为0,输出一个0即可
44 .put_each_num:
45    push ecx
46    call put_char
47    add esp,4
48    inc edi                 ;使edi指向下一个字符
49    mov cl,[put_int_buffer+edi]
50    cmp edi,8
51    jl .put_each_num
52    popad
53    ret
复制代码

然后是print.h(完整的):

复制代码
1 #ifndef __LIB_KERNEL_PRINT_H
2 #define __LIB_KERNEL_PRINT_H
3 #include "stdint.h"
4 void put_char(uint8_t char_asci);
5 void put_str(char* message);
6 void put_int(uint32_t num);
7 #endif
复制代码

 再是main.c

复制代码
  put_char('\n')
  put_int(0x00000000);
  put_char('\n');
  put_int(0x12345678);
  put_char('\n');
  put_int(0xa1a20099);
  put_char('\n');
复制代码

 让我们来看看成果:

嘿嘿,感觉自己这个还是挺成功的。


参考博客:


结束语:

看到上方第一个博客作者的学习经历还是颇有感触的。记得我在大二结束的暑假第一次看《Operating System:Three Easy Pieces》时,看到书中有段C代码想要尝试,需要包含头文件"unistd.h",当时的我还没怎么接触过Linux,于是傻傻地在自己Win10的IDE中抄下并运行这段代码,结果当然是报错没有找到头文件,当时还十分疑惑遂不了了之。现在看到这个博主在大一暑假就已经在接触Linux、写一个OS,感觉自己和他的差距真的挺大的。博主出身重邮,而我就读于某211,但是。。。怎么说呢,感觉周围很多同学也都没有博主“启蒙”得早吧——能够在大一就学习这么多东西。而我自己也半斤八两吧,大一的时候也从没想过这么多,只是平庸地学习着学校的课程,现在回想起来,感觉有那么多课都是无意义的,占据了我们非常多的时间。当然,也很羡慕博主有破釜沉舟的勇气,他提到自己在期末考最后十天才开始学习高数大物,可想前面的时光大抵都在研究这些计算机基础知识。而我周围的同学们(包括我)要不陷入疲乏的内卷、要不就沉溺于教室后排玩手机。或许博主是幸存者偏差,所以我不愿说是学校的差距或者学习氛围的差距,但作为同龄人,他也确确实实让我看到了我们两个人之间的差距。哎,如今越长大,越感到时间流逝得飞快,敲敲代码看看书一天就过去。“对未来最大的慷慨,就是把一切都献给现在。”最后,愿诸君共勉。——来自大三下时期的我的有感而发。

posted @   Hell0er  阅读(193)  评论(0编辑  收藏  举报
编辑推荐:
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
阅读排行:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示