5.库函数 和 调试打印函数

基础部分在前四章已经介绍完了。

5.1 c语言字符串处理函数

在内核中无法使用 处于用户态的c语言库函数 所以需要自己实现相关函数

include/string.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#ifndef INCLUDE_STRING_H_
#define INCLUDE_STRING_H_
#include "typers.h"

// 函数声明
// 将 src 指向的内存区域的前 len 个字节复制到 dest 指向的内存区域。
void memcpy(uint8_t *dest, const uint8_t *src, uint32_t len);

// 将 dest 指向的内存区域的前 len 个字节设置为 val 指定的值。
void memset(void *dest, uint8_t val, uint32_t len);

// 将 dest 指向的内存区域的前 len 个字节清零(等同于调用 memset(dest, 0, len))
void bzero(void *dest, uint32_t len);

// 比较两个字符串 str1 和 str2。
int strcmp(const char *str1, const char *str2);

// 将字符串 src(包括终止 null 字符)复制到 dest。
char *strcpy(char *dest, const char *src);

// 将字符串 src 连接到字符串 dest 的末尾,覆盖 dest 最后的 null 终止符,并在最后添加一个新的 null 终止符。
char *strcat(char *dest, const char *src);

// 计算字符串 src 的长度,不包括终止的 null 字符。
int strlen(const char *src);

#endif // INCLUDE

函数实现

libs/string.c

memcpy()

1
2
3
4
5
6
7
8
inline void memcpy(uint8_t *dest, const uint8_t *src, uint32_t len){
// 异常处理
if (dest == NULL || src == NULL || len ==0) return;

for(; len != 0; len--) {
*dest++ = *src++; // ++ 优先级高于 * 先右边结合 d++ 然后再 *d 最后在++进行自增 指向下一个内存地址
}
}

使用内联函数 减少函数调用的开销

++ 优先级高于 * 先右边结合 d++ 然后再 *d 最后在++进行自增 指向下一个内存地址

  • 解引用 dest 当前指向的内存位置,用于读取或写入值。
  • dest 指针移动到下一个 uint8_t 类型的内存地址。

memset()

1
2
3
4
5
6
7
8
inline void memset(void *dest, uint8_t val, uint32_t len) {
//if (dest == NULL || len == 0) return;

uint8_t *dst = (uint8_t *) dest;
for (; len != 0; len--) {
*dst++ = val;
}
}
  • uint8_t *dst = (uint8_t *) dest;
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    - 这行代码中的类型转换是必要的,因为 `void*` 类型的指针是一个通用指针,无法直接进行算术运算。将 `void*` 转换为 `uint8_t*` 使得可以按字节操作内存(即每次操作一个字节)。
    - `uint8_t` 是一个确保只操作一个字节大小的类型,适用于按字节设置内存值。



    > bzero() 将 dest 指向的内存区域的前 len 个字节清零(等同于调用 memset(dest, 0, len))

    ```c
    inline void bzero(void *dest, uint32_t len){
    memset(dest, 0, len);
    }

strcmp()

1
2
3
4
5
6
7
8
9
static inline int strcmp(const char *str1, const char *str2)
{
while (*str1 && *str2 && *str1 == *str2) {
str1++;
str2++;
}

return *str1 - *str2;
}

*str1 && *str2 确保当前指针指向的字符串不是终止字符 \0

*str1 == *str2 确保相等

返回值:return *str1 - *str2; 返回ASCII之差

  1. 如果 *str1 - *str2 为零(即 *str1 == *str2),这意味着两个字符串相等或在某一点同步到了字符串的终止字符。
  2. 如果 *str1 - *str2 为正值,则 str1 在首个不同点处的字符在字典顺序中位于 str2 的对应字符之后。
  3. 如果 *str1 - *str2 为负值,则 str1 在首个不同点处的字符在字典顺序中位于 str2 的对应字符之前。

strcpy()

1
2
3
4
5
6
7
8
9
10
11
12
static inline char *strcpy(char *dest, const char *src)
{
char *tmp = dest;
// 当src 不为终止符的时候继续
while (*src) {
*dest++ = *src++;
}

*dest = '\0'; // 终止符

return tmp;
}

strcat()

1
2
3
4
5
6
7
8
9
10
11
12
13
static inline char *strcat(char *dest, const char *src)
{
char *cp = dest;

while (*cp) {
cp++;
}

while ((*cp++ = *src++))
;

return dest;
}

cp++; 找到字符串末尾

*cp++ = *src++ 进行赋值

strlen()

1
2
3
4
5
6
7
8
9
static inline int strlen(const char *src)
{
const char *eos = src;

while (*eos++)
;

return (eos - src - 1);
}

(eos - src - 1) 因为地址是连续的,所以直接进行地址相减

5.2 内核级打印函数 printk

参照printf 函数:首先是一个待显示的字符 串,里面分别用%加相关字母的方式一一指明了后面的参数数量和类型。只要我们传递正确 的带有格式描述的字符串和相关参数,printf函数就能正确的打印出来结果。

5.3 利用qemu 联合 gdb 进行c语言 源代码级别调试

通讯机制:

qemu -S -s -fda floppy.img -boot a

  1. 注意这里的qemu 要替换成 qemu-system-i386
  2. 因为qemu和gdb运行的时候毕竟是两个进程,数据交换必然涉及到进 程间通信机制。
  3. -fda floppy.img 和 -boot a 是指定启动的镜像,-s 这个参数指的 是启动时开启1234端口等待gdb连接(这个参数从字面上看比较隐晦),-S 是指是启动时 不自动开始运行,等待调试器的执行命令。

启动gdb后执行下面命令: — 后续直接封装在gdbinit这个文件中

1
2
3
4
file hx_kernel
target remote :1234
break kern_entry
c

执行命令

1
2
make
make debug

5.4 打印函数调用的栈信息 — multiboot

image-20240510170440547

在示意图中我们假设从start函数->kern_entry函数->console_clear函数的调用过 程,最终暂停在console_clear函数里面。我们可以清楚的看到,只要拿到此时的ebp寄存 器的值,就可以沿着这个调用链找到每一个调用的函数的返回地址,之前的问题就这样解 决了。需要注意的是C语言里对指针做算数运算时,改变的地址长度是和当前指针变量的类 型相关的。

5.4 修改entry.c函数

entry.c

1
2
3
4
5
6
7
8
9
10
11
#include "types.h"
#include "console.h"
#include "debug.h"

int kern_entry(){
init_debug();
console_clear();
console_write_color("Hello, OS kernel!\n", rc_black, rc_green);
panic("test");
return 0;
}

运行结果:

image-20240510170638484