从0开始写内核(六)完善内核
参考书籍:操作系统真相还原
源码:https://github.com/wutiaojian000/AFKernel.git
本文地址:https://www.cnblogs.com/angel-fish/p/18895173
1. 函数调用约定

咱会用到cdecl,看看下面的例子就好。

1.2 汇编与C混合编程
有两种方法,一种是C和汇编单独编译成目标文件后再一起链接,另一种是C中嵌入汇编代码一起编译。
//C_with_S.c
extern void asm_print(char*, int);
void c_print(char* str)
{
int len = 0;
while(str[len++]);
asm_print(str, len);
}
;C_with_S_S.S
section .data
str: db "asm_print says hello!", 0xa, 0
str_len equ $ - str
section .text
extern c_print
global _start
_start:
push str
call c_print
add esp, 4
mov eax, 1 ;1号子功能是exit系统调用
int 0x80
global asm_print
asm_print:
push ebp
mov ebp, esp
mov eax, 4
mov ebx, 1
mov ecx, [ebp+8]
mov edx, [ebp+12]
int 0x80
pop ebp
ret
2. 实现自己的打印函数
以下是显卡的寄存器目录。

前4组是寄存器分组,被分为了两类寄存器,address register和data register。address register用来输入寄存器在该寄存器分组的下标,data register用来输入/出数据。
CRT controller register中的address/data register的端口地址由miscellaneous output register中的input/output address select字段决定。

I/OAS会影响上面寄存器分许中所有端口地址带x的寄存器,为0时,那些端口地址会被设置为0x3bx;为1时,那些端口地址会被设置为0x3dx。miscellaneous output register寄存器默认值是0x67,I/OAS位最重要,这里是设置为1。
2.1 实现单个字符的打印
我们要实现put_char函数,用于打印一个字符。先定义一下标准数据类型。
// kernel/lib/stdint.h
#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
打印函数全都放在print.S中。
; kernel/lib/print.S
TI_GDT equ 0
RPL0 equ 0
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0
[bits 32]
section .text
global put_char
put_char:
pushad ;将通用寄存器压栈
mov ax, SELECTOR_VIDEO
mov gs, ax ;不能直接为段寄存器赋值
;获取光标当前位置
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 ;用于提供光标位置的低8位
out dx, al
mov dx, 0x03d5 ;通过读写数据端口0x3d5来获得或设置光标的位置
in al, dx ;得到光标位置的低8位
;将光标存入bx
mov bx, ax
;在栈中获取待打印的字符
mov ecx, [esp + 36] ;前面压入了8个通用寄存器和返回地址
cmp cl, 0xd
jz .is_carriage_return
cmp cl, 0xa
jz .is_line_feed
cmp cl, 0x8 ;退格符的ascii码
jz .is_backspace
jmp .put_other
.is_backspace:
dec bx
shl bx, 1 ;光标实际位置是下标*2
mov byte [gs:bx], 0x20 ;将删除的字节补为空格
inc bx
mov byte [gs:bx], 0x07
shr bx, 1
jmp .set_cursor
.put_other:
shl bx, 1
mov byte [gs:bx], cl ;ascii码本身
inc bx
mov [gs:bx], 0x07
shr bx, 1
inc bx
cmp bx, 4000 ;书里这里写错了
jl .set_cursor ;如果光标值小于2000,表示没有写到显存的最后,则设置新的光标值;若超出则换行处理
.is_line_feed:
.is_carriage_return:
;如果是CR(\r),只要把光标移到行首即可
xor dx, dx ;dx是被除数的高16位
mov ax, bx ;ax是被除数的低16位
mov si, 0x80
div si
sub bx, dx ;光标值减去除80的余数就是取整
.is_carriage_return_end:
add bx, 80
cmp bx, 80
.is_line_feed_end:
jl .set_cursor
.roll_screen:
cld
mov ecx, 960
mov esi, 0xc00b80a0 ;第1行
mov edi, 0xc00b8000 ;第0行
rep movsd
;将最后一行填充为空白
mov ebx, 3840
mov ecx, 80
.cls:
mov word [gs:ebx], 0x0720 ;空格键
add ebx, 2
loop .cls
mov bx, 1920 ;重设置为最后一行的首字符
.set_cursor:
;将光标设置为ebx 设置高8位
mov dx, 0x03d4
mov al, 0x0e
out dx, al
mov dx, 0x03d5
mov al, bh
out dx, al
;在设置低8位
mov dx, 0x03d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
mov al, bl
out dx, al
.put_char_done:
popad
ret
这里的pushad是备份通用寄存器的指令,将这些寄存器的值压栈,顺序是EAX->ECX->EDX->EBX->ESP->EBP->ESI->EDI。
in指令如果是8位数据,寄存器一定是用低8位如al。
其他的就不说明了,看书很容易理解。
// kernel/include/print.h
#ifndef __LIB_KERNEL_PRINT_H
#define __LIB_KERNEL_PRINT_H
#include "stdint.h"
void putchar(uint8_t char_asci);
#endif
// kernel/src/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);
}
nasm -f elf -o out/print.o kernel/lib/print.S
gcc -m32 -I kernel/include/ -c -o out/main.o kernel/src/main.c
ld -m elf_i386 -Ttext 0xc0001500 \
--section-start .rodata=0xc0002000 \
-e main -o kernel.bin \
out/main.o out/print.o
dd if=kernel.bin of=/home/zcm/bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc

2.2 实现字符串打印
;kernel/lib/print.S
[bits 32]
section .text
global put_str
put_str:
push ebx
push ecx
xor ecx, ecx
mov ebx, [esp + 12]
.goon:
mov cl, [ebx]
cmp cl, 0
jz .str_over
push ecx
call put_char
add esp, 4
inc ebx
jmp .goon
.str_over:
pop ecx
pop ebx
ret
// kernel/lib/print.h
void put_str(char* message);
// kernel/src/main.c
#include "print.h"
void main(void)
{
put_str("kernel.\n");
while(1);
}
突然发现之前写的loader.S只加载了text段,没有加载别的段,然后这里的字符串常量没有加载进来,再改一下。
把以前代码的这几段注释掉,
;kernel/src/loader.S
;mov eax, [ebx + 24]
;and eax, 0x00000001
;cmp eax, 0x00000000
;je .PTNULL
;mov ecx, 0x00000001

2.3 实现整数打印
section .data
put_int_buffer dq 0
;...
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位数字中有8个16进制数
mov ebx, put_int_buffer
.16based_4bits:
and edx, 0x0000000f
cmp edx, 9
jg .is_A2F
add edx, '0'
jmp .store
.is_A2F:
sub edx, 10
add edx, 'A'
.store:
mov [ebx + edi], dl
dec edi
shr eax, 4
mov edx, eax
loop .16based_4bits
;把高位的0给去掉
.ready_print:
inc edi ;edi变成0xffffffff了,加上1变成0
.skip_prefix_0:
cmp edi, 8
je .full0
.go_on_skip:
mov cl, [put_int_buffer + edi]
inc edi
cmp cl, '0'
je .skip_prefix_0
dec edi
jmp .put_each_num
.full0:
mov cl, '0'
.put_each_num:
push ecx
call put_char
add esp, 4
inc edi
mov cl, [put_int_buffer + edi]
cmp edi, 8
jl .put_each_num
popad
ret
// kerne/include/print.h
void put_int(uint32_t num);
// kernel/src/main.c
#include "print.h"
void main(void)
{
put_str("kernel.\n");
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);
while(1);
}

3. 内联汇编
3.1 基本内联汇编
基本内联汇编的格式如下,
asm [volatile] ("assembly code")
asm和__asm__都可以。volatile表示原样保留这段代码。指令之间用';',','或换行符'\n'或换行符加制表符'\n\t'。
汇编中要想引用c变量,只能用全局变量。
char *str = "hello world\n";
int count = 0;
void main()
{
asm("\
movl $4, %eax;\
movl $1, %ebx;\
movl str, %ecx;\
movl $12, %edx;\
int 0x80;\
mov %eax, count;\
popa
");
}
3.2 扩展内联汇编
格式为
asm [volatile] ("assembly code":output:input:clobber/modify)
output用来指定汇编代码的数据怎么输出给c使用;
input用来指定c的数据怎么输入给汇编使用;
clobber/modify通知编译器,可能造成寄存器或内存数据的损坏,做好保护。
(1) 寄存器约束
常见的寄存器约束有:
a:表示寄存器eax/ax/al;
b:表示寄存器ebx/bx/bl;
c:表示寄存器ecx/cx/cl;
d:表示寄存器edx/dx/dl;
D:表示寄存器edi/di;
S:表示寄存器esi/si;
q:表示这任意4个通用寄存器中的一个eax/eba/ecx/edx;
r:表示这任意4个通用寄存器中的一个eax/eba/ecx/edx/esi/edi;
g:可以存放在任何地点,寄存器或内存;
A:把eax和edx组合成64位整数;
f:表示浮点寄存器;
t:表示第一个浮点寄存器;
u:表示第二个浮点寄存器。
举个例子,
#include<stdio.h>
void main(void)
{
int in_a = 1, in_b = 2, out_sum;
asm("addl %%ebx, %%eax":"=a"(out_sum):"a"(in_a), "b"(in_b));
printf("%d\n", out_sum);
}
有一点verilog的那种感觉。
(2) 内存约束
内存约束指直接将c中的变量做为汇编的操作数,也就是直接操作c的指针。
m:表示操作弧可以用任意一种内存形式;
o:操作数为内存变量,但访问它必须通过偏移。
举个例子,
#include <stdio.h>
void main()
{
int in_a = 1, in_b = 2;
printf("%d\n", in_b);
asm("movb %b0, %1;"::"a"(in_a),"m"(in_b));
printf("%d\n", in_b);
}
注意不允许内存到内存的约束。
(3) 立即数约束
立即数约束要求在gcc在船只时不通过内存和寄存器,做为直接数传给汇编代码。
i:操作数为整数立即数;
F:操作数为浮点数立即数;
I:操作数为0-31之间的立即数;
J:操作数为0-63之间的立即数;
N:操作数为0-255之间的立即数;
O:操作数为0-32之间的立即数;
X:操作数为任何立即数。
(4) 通用约束
只用在input中,但可以表示与output和input中第个操作数用相同的寄存器或内存。
有时候约束不是那么严格,比如r,我们不知道用的到底是哪个寄存器,这时候就需要占位符。占位符分为两种,序号占位符和名称占位符。序号占位符是对output和input中的操作数从左到右按顺序编号,最多支持10个,0-9,格式是%0-9。
asm("addl %2, %1":"=a"(out_sum):"a"(in_a), "b"(in_b));
占位符代表的操作数默认是32位的,但针对不同的指令会有变化。32位操作数的指令自然是32位数据;16位操作数指令取数据的低16位,高16位不能用;8位操作数可以用0-7位,也可以用8-15位,%和序号间插入b表示低8位(默认是低8位),插入h表示高8位。
asm("movb %1, %0;":"=m"(in_b):"a"(in_a));//传入a的0x78
asm("movb %h1, %0;":"=m"(in_b):"a"(in_a));//传入a的0x56
名称占位符,需要在约束中起名字。
asm("divb %[divisor];movb %%al, %[result]":[result]"=m"(out):"a"(in_a), [divisor]"m"(in_b));
约束中还有操作数类型修饰符,在output中有以下3种:
- =表示只写;
- +表示操作数是可读写的,所约束的寄存器或内存先被读入再被写入;
- &表示操作数独占这个寄存器,不能在分配给input的操作数,有多个修饰符时&需要与约束名挨着。
在input中,
%该操作数可以和下一个输入操作数互换。
asm("addl %%ebx, %%eax;":"+a"(in_a):"b"(in_b));
函数执行完成前,返回值会保存在eax中,如果input约束中将eax分配给了某个操作数就会覆盖掉返回值,用&可以防止某些寄存器比如eax被分配给input操作数。
asm("movl $6, %2;":"=&a"(ret_cnt):"r"(test));
这样分配到test的寄存器就不会是eax了。
clobber/modify用于告诉编译器修改了哪些寄存器,好提前保存起来。
asm("movl %%eax, %0;movl %%eax, %%ebx":"m"(ret_value)::"bx");
只用写bx甚至是bl,会把整体保护起来。
如果会修改到eflags,就用"cc";"memory"用来告诉编译器哪块内存被修改了,还有就是读内存的时候可能用寄存器中缓存的数据,memory告诉编译器直接从内存中读,不走缓存,类似volatile。
3.3 扩展内联汇编之机器模式
之前的'h'、'b'也是属于这个内容。机器模式是用来在机器层面上指定数据的大小和格式。


这一节暂时没看太明白。

浙公网安备 33010602011771号