操作系统真象还原实验记录之实验八:实现打印字符串函数
书P276
本次实验就是上次实验打印字符的连续调用。
1.相关基础知识
c编辑器会把字符串结尾自动填上0,并为该字符串分配连续的内存,如果传递参数是字符串的话,那么传递的是字符串的内存首地址。
1.2 内联汇编
内联汇编采用AT&T语法
1.2.1 AT&T语法
segreg(段基址):base_address+ offest_address+index*size
或segreg:base_address(offset_address,index,size)
base_address为整数,变量名,可正可负
offset_address、index必须是8个通用寄存器之一
size只能是1、2、4、8
AT&T语法普通数字是内存地址,表示立即数要加$
intel语法普通数字就是立即数,表述内存才需要加[]
1.2.2内联汇编语法规则
格式:
asm [volatile] (“assembly code” : output: input: clobber/modify)
asm 声明内联汇编,不可少,其余项均可省略
volatile:表示gcc编译时不要修改我的汇编代码,可省,gcc有个-O的优化选项,可能会改变汇编代码。
assembly code :就是要写的汇编代码,一句一个分号,换行要用\
output/input:“约束”(C变量名),
input表汇编代码运行前C变量要提前输入到内联汇编的寄存器且C变量不能修改("+“约束除外)
output表寄存器的代码运行结果要输出到C变量,不能取出C变量的数据使用(若加了”+"约束,就是告诉内联汇编,这个C变量的数据你既可以用,也可以结束最后修改,所以就从input就合并到output)
如果是内存,则如下,m表示C变量提供指针
m在input表示这个指针指向的内存数据只能读出,不能修改
m在output表示这个指针指向的内存数据不能被读出,只能修改
clobber/modify:指明破坏的寄存器或内存,已在input和ouput无需指明。
约束:
a表eax/ax/al;b、c、d同理
D表edi/di
S表esi/si
q表eax/ebx/ecx/edx任意一个
r表eax/ebx/ecx/edx/edi/esi任意一个
g表任意寄存器或内存
立即数约束:
N:表示C变量是0到255之间的立即数
等
通用约束:
0到9:表示可与output和input中第n个寄存器或内存相同
序号占位符:
0到9,最多支持10个序号占位符,要加一个%,故寄存器要加两个%加以区分
名称占位符:
[名称]“约束”(C变量)
=:表示C变量或C变量指针所指的内存数据只能修改,不能被读出供内联汇编使用。
+: 表示C变量先被读入使用,最后再被运行结果修改
也表示C变量内存指针所指内存数据先被读出使用,最后被写入修改。
&:表示output独占某寄存器
%:表示两个C变量可以互换,一般用于加法乘法。
h表ah、bh、ch、dh
b表al、bl、cl、dl
w表ax、bx、cx、dx
k表eax、ebx、ecx、edx
/* 将从端口port读入的word_cnt个字写入addr */
static inline void insw(uint16_t port, void* addr, uint32_t word_cnt) {
/******************************************************
insw是将从端口port处读入的16位内容写入es:edi指向的内存,
我们在设置段描述符时, 已经将ds,es,ss段的选择子都设置为相同的值了,
此时不用担心数据错乱。*/
asm volatile ("cld; rep insw" : "+D" (addr), "+c" (word_cnt) : "d" (port) : "memory");
/******************************************************/
}
赋值端口给dx,目的地址给edi,循环次数给ecx, insw 配合 rep,完成多次从dx端口读出数据转移带edi内存地址,每一次edi要自增2。
功能用内联汇编指令完成, 端口port,目的地址addr, 循环次数word_cnt都是c函数要接受的参数,属于局部变量,这些变量显然要读出到汇编,供汇编使用。
理论上,三个参数应该放在input里,不需要加号,rep insw无返回值。
但是这里如果放在output里,就必须加+号了,让两个参数也具备输入寄存器功能
最后指明内存被破坏。
告知c语言本内联汇编代码可能破坏的寄存器和内存,gcc编译器可以提前规避寄存器占用的风险,因为gcc给c程序分配的寄存器是未知的,汇编代码无法规避;
以及
c程序使用的内存,汇编程序员是已知的
gcc编译时,有时会将内存数据缓存到寄存器,编译结束再写回内存处,
如果一段内联汇编程序修改了内存数据,那么接下来编译器应该按照这部分修改后的内存数据继续编译,所以寄存器内的就内存数据是错误的,gcc应该重新读内缓存
所以内联汇编也要指明内存是否破坏。
2.实验代码
2.1 print.h
#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
2.2 main.c
#include "print.h"
void main(void){
put_str("i am kernel");
put_int(0);
put_char('\n');
put_int(9);
put_char('\n');
put_int(0x00021a3f);
put_char('\n');
put_int(0x12345678);
put_char('\n');
put_int(0x00000000);
put_char('\n');
while(1);
}
2.3 print.s
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
;-------------------- 将小端字节序的数字变成对应的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
;------------------------ 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
功能总结
1.增加函数put_str
main.c压栈传递过来的字符串首地址,位于esp+12,依次遍历每个字符,如果为0,说明打印完毕,否则调用put_char打印此字符。
2.增加函数put_int
该函数传入的参数是16进制的整数,比如
put_int(9);
传入的是9,实际上put_int接收到的是00000009这8位16进制数
put_int(0x00021a3f);
put_int接收到的参数是00021a3f这8位16进制数
最终打印的也是这八个16进制数对应的字符
先[esp+4*9]找到传入的8个16进制数,用32位寄存器eax来1保存每一位
然后对每一个16进制数也就是每4位二进制数进行处理,
0~ 9或者a~ f加上偏移量转化成对应的字符的ascll码值,
由高向低存入定义好的内存缓冲区。
然后从低向高检查第一个非0,以便去掉前缀的多余的0.
最后调用put_char依次打印缓冲区内容。
3.实验结果
实验步骤一律同上篇博客