《操作系统真象还原》第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');
让我们来看看成果:
嘿嘿,感觉自己这个还是挺成功的。
参考博客:
- 《操作系统真象还原》第六章 ---- 开启c语言编写函数时代 首挑打印函数小试牛刀 费心讨力重回gcc降级 终尝多日调试之喜悦_Love 6的博客-CSDN博客
- 《操作系统真象还原》第六章_Atropos998的博客-CSDN博客
结束语:
看到上方第一个博客作者的学习经历还是颇有感触的。记得我在大二结束的暑假第一次看《Operating System:Three Easy Pieces》时,看到书中有段C代码想要尝试,需要包含头文件"unistd.h",当时的我还没怎么接触过Linux,于是傻傻地在自己Win10的IDE中抄下并运行这段代码,结果当然是报错没有找到头文件,当时还十分疑惑遂不了了之。现在看到这个博主在大一暑假就已经在接触Linux、写一个OS,感觉自己和他的差距真的挺大的。博主出身重邮,而我就读于某211,但是。。。怎么说呢,感觉周围很多同学也都没有博主“启蒙”得早吧——能够在大一就学习这么多东西。而我自己也半斤八两吧,大一的时候也从没想过这么多,只是平庸地学习着学校的课程,现在回想起来,感觉有那么多课都是无意义的,占据了我们非常多的时间。当然,也很羡慕博主有破釜沉舟的勇气,他提到自己在期末考最后十天才开始学习高数大物,可想前面的时光大抵都在研究这些计算机基础知识。而我周围的同学们(包括我)要不陷入疲乏的内卷、要不就沉溺于教室后排玩手机。或许博主是幸存者偏差,所以我不愿说是学校的差距或者学习氛围的差距,但作为同龄人,他也确确实实让我看到了我们两个人之间的差距。哎,如今越长大,越感到时间流逝得飞快,敲敲代码看看书一天就过去。“对未来最大的慷慨,就是把一切都献给现在。”最后,愿诸君共勉。——来自大三下时期的我的有感而发。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库