操作系统真象还原实验记录之实验八:用c与汇编混合编程实现打印字符函数
对应书P274
1.相关背景知识
1.1 函数调用约定
stdcall(未使用):
(1)调用者将所有参数从右向左入栈
(2)被调用者清理参数所占栈空间
cdecl(c语言默认的约定):
(1)调用者将所有参数从右向左入栈
(2)调用者清理参数所占栈空间
1.2 c语言和汇编语言混合编程
必须牢记c语言遵循的cdecl函数调用约定。
比如汇编程序希望调用c语言写的函数,知道cdecl约定,你才能知道汇编程序是调用者,要自己从右向左传参,调用结束还要add esp恢复栈空间。
BIOS中断走的是中断向量表,Linux系统调用走的是IDT中的第0x80项。
下面例子使用Linux系统调用,int 0x80 第四号子功能,模拟c语言库函数使用该系统调用和直接使用write系统调用
c语言调用自己的库函数,本质上也是库函数实现了系统调用。
上述c语言调用自己的库函数是用汇编模拟的,正常c语言使用c代码调用自己的库函数比如printf
下面模拟的是c代码调用汇编代码写的函数,汇编代码调用c语言写的函数
在汇编代码中导出符号供外部引用是用关键字global,引用外部文件的符号是用关键字extern
在c代码中,将符号定义为全局便可以被外部引用,一般无需额外关键字,引用外部符号时用extern声明。
1.3 显卡的端口控制
显卡I/O接口内部的寄存器非常多,但是分到的I/O端口地址有限。
因此,Address Register作为数组的索引,对应内部的某个寄存器,
Data Register是该数组索引对应的寄存器的窗口,对Data Register窗口读写的数据都作用在索引对应的内部某个寄存器上。
这次实验就对光标寄存器进行了操作
2.实验代码
2.1 print.s
TI_GDT equ 0
RPL0 equ 0
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0
[bits 32]
section .text
;------------------------ put_char -----------------------------
;功能描述:把栈中的1个字符写入光标所在处
;-------------------------------------------------------------------
global put_char
put_char:
pushad ;备份32位寄存器环境
;需要保证gs中为正确的视频段选择子,为保险起见,每次打印时都为gs赋值
mov ax, SELECTOR_VIDEO ; 不能直接把立即数送入段寄存器
mov gs, ax
;;;;;;;;; 获取当前光标位置 ;;;;;;;;;
;先获得高8位
mov dx, 0x03d4 ;索引寄存器
mov al, 0x0e ;用于提供光标位置的高8位
out dx, al
mov dx, 0x03d5 ;通过读写数据端口0x3d5来获得或设置光标位置
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, 0xc00b80a0 ; 第1行行首
mov edi, 0xc00b8000 ; 第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
功能总结:
只有一个函数put_char:
每次调用put_char提供一个字符参数
比如
put_char(‘K’);
该函数对输入的字符进行如下流程的判断
如果为回车换行、后退,则直接执行相应函数。其中回车换行后光标位置大于等于2000,也会执行滚屏。
2.2 main.c
#include "print.h"
void main(void)
{
put_char('K');
put_char('e');
put_char('r');
put_char('n');
put_char('e');
put_char('l');
put_char('\n');
put_char('1');
put_char('2');
put_char('\b');
put_char('3');
while(1);
}
2.3 stdint.h
注意此文件放在lib/kernel中,而不是书上所说的lib中,不然步骤6会报错
#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
2.4 print.h
#ifndef __LIB_KERNEL_PRINT_H
#define __LIB_KERNEL_PRINT_H
#include "stdint.h"
void put_char(uint8_t char_asci);
#endif
这里看似传入参数是一个字节,但实际汇编代码被翻译成了4字节入栈,gcc将c语言编译成等价汇编代码时,默认将push跟的立即数识别成32位。
正常情况下,print.S中的put_char函数对于其他文件来说属于外部函数,其他c程序文件引用时需要声明extern,但是这里显然没有加extern,c代码也能调用外部函数put_char。
#ifndef __LIB_KERNEL_PRINT_H
#define __LIB_KERNEL_PRINT_H
#endif
是为了防止头文件被重复包含,避免头文件变量被重复定义
以print.h所在路径定义了__LIB_KERNEL_PRINT_H,用于判断这个头文件是否包含,__LIB_KERNEL_PRINT_H这个宏就等于print.h的文件路径。
3 实验步骤
1.编译loader.s
nasm -o loader.bin loader.s
2.编译mbr.s
nasm -o mbr.bin mbr.s
3.将mbr.bin刻入第0扇区
dd if=/home/Seven/bochs2.68/bin/mbr.bin of=/home/Seven/bochs2.68/bin/Seven.img bs=512 count=1 seek=0 conv=notrunc
4.将loader.bin刻入第2扇区
dd if=/home/Seven/bochs2.68/bin/loader.bin of=/home/Seven/bochs2.68/bin/Seven.img bs=512 count=3 seek=2 conv=notrunc
5.编译print.s
nasm -f elf -o lib/kernel/print.o lib/kernel/print.s
6.编译main.c
gcc -m32 -I lib/kernel/ -c -o kernel/main.o kernel/main.c
gcc -I 指的是指明头文件相对或者绝对路径 print.h和stdlib.h均要在 lib/kernel/这个相对路径下,
否则报错(书上写的stdlib.h相对路径为lib,报如下错)
找不到c需要的库
7.链接main.o与print.o
ld -m elf_i386 -Ttext 0xc0001500 -e main -o kernel.bin kernel/main.o lib/kernel/print.o
8.kernel.bin刻入硬盘
dd if=kernel.bin of=/home/Seven/bochs2.68/bin/Seven.img bs=512 count=200 seek=9 conv=notrunc
9.模拟运行
./bochs -f bochsrc.disk
4 实验结果
5.第17行代码 赋值gs 特权级分析
put_char函数内部给gs重新赋值RPL=0的选择子,是为了以后用户进程调用提前的准备,目前的mbr.s、loader.s、main.c均是内核程序,CPL=0,任何数据段寄存器RPL=0,不会出问题。
未来的代码会实现用户进程的切换,会利用iret指令弹栈的方式修改cs,从而完成切换。切换后如果对于用户进程特权级为3,如果gs.RPL=0,那么gs会指向哑段描述符。这个时候如果用户进程调用put_char,使用gs访存会报异常。所以要在put_char函数里重新让gs指向DPL=0的显存段。