操作系统真象还原实验记录之实验八:用c与汇编混合编程实现打印字符函数

本文档详述了一个操作系统实验,涉及C与汇编混合编程实现打印字符的函数put_char。实验中,put_char函数根据输入字符执行不同操作,如普通字符输出、回车换行、退格等,并处理屏幕滚动。实验代码包括汇编实现的put_char函数和C语言的main函数调用。同时,文中解释了函数调用约定、显卡端口控制以及实验步骤。
摘要由CSDN通过智能技术生成

操作系统真象还原实验记录之实验八:用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’);
该函数对输入的字符进行如下流程的判断

Created with Raphaël 2.2.0 如果输入为一般字符 检查光标位置是否大于等于2000? 说明光标位置等于2000,执行回车、换行,光标位置变为2080(第一个位置为0号) 2080>=2000,执行滚屏,光标位置重新设置为1920,即最后一行行首 .执行set_cursor yes no

如果为回车换行、后退,则直接执行相应函数。其中回车换行后光标位置大于等于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的显存段。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值