从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种:

  1. =表示只写;
  2. +表示操作数是可读写的,所约束的寄存器或内存先被读入再被写入;
  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'也是属于这个内容。机器模式是用来在机器层面上指定数据的大小和格式。


这一节暂时没看太明白。

posted @ 2025-05-27 16:38  横渡大海的神仙鱼  阅读(35)  评论(0)    收藏  举报