从裸机启动开始运行一个C++程序(十二)

前序文章请看:
从裸机启动开始运行一个C++程序(十一)
从裸机启动开始运行一个C++程序(十)
从裸机启动开始运行一个C++程序(九)
从裸机启动开始运行一个C++程序(八)
从裸机启动开始运行一个C++程序(七)
从裸机启动开始运行一个C++程序(六)
从裸机启动开始运行一个C++程序(五)
从裸机启动开始运行一个C++程序(四)
从裸机启动开始运行一个C++程序(三)
从裸机启动开始运行一个C++程序(二)
从裸机启动开始运行一个C++程序(一)

重新整理工程文件

到目前为止,我们工程中的源文件有两类,一类是.nas结尾的汇编代码,另一类是.c结尾的C语言代码,他们之间还有互相调用的关系(kernel.nas调用entry.c,而entry.c会调用asm_fun.nas)。

同时,我们把跟MBR和Kernel头部的部分,以及管理C程序相关的库都是混在一起写的,后续如果工程再复杂起来,会显得比较凌乱不好管理。所以,在继续之前,我们先做这么几件事:

  1. 把函数声明、结构体定义等收纳到头文件中管理。
  2. 分离MBR、Kernel、C库相关部分,在独立路径中管理(并编写对应的makefile
  3. 将C库的部分先整理为静态链接库(lib),之后再参与编译。

整理后的路径如下:
调整后的工程路径

这里调整后的路径会上传到附件中(12-1),建议读者可以对照着工程来看。

根目录下我们保留bochsrc,这是配置虚拟机的。然后里面分别有mbrkernellibc三个路径。前两个无需解释,后面这个libc就是我们把一些C库相关的东西写在这个路径里,而kernel逻辑相关的则是放在kernel路径下,例如kernel/entry.c

与此同时,我们将挤在entry.c当中的putcharputs相关逻辑转移至libc/stdio.c中,并且在libc/include/stdio.h中进行声明。

之后,在编译选项中通过-I参数,可以将默认的头文件搜索路径定向到libc/include中。并且针对libc路径单独进行静态链接库打包,成为libc.a

最后,在链接选项中,通过-L参数指定静态库搜索路径为libc,并且通过-l参数指定使用libc.a静态库。

再次强调,请读者通过附件中的工程代码,仔细阅读一下改造后的工程布局和对应的makefile。由于文章中不便表示这种路径调整的动作,因此不再在正文中引用代码,请读者通过工程实例来查看。

继续完善C库

接下来我们要继续完善C库,实现几个重要的功能,让代码库至少处于一个基本可用状态。

stddef

这个库主要是实现一些宏和类型定义:

// stddef.h
#ifndef NULL
#define NULL (void *)0
#endif

typedef unsigned size_t;
typedef unsigned uintptr_t;

stdint

这个库主要是实现一些定长整型。注意,当前我们是在32位环境下,日后切换到64位环境后要做一定的适配。

typedef char int8_t;
typedef short int16_t;
typedef int int32_t;
typedef long long int64_t;

typedef unsigned char uint8_t;
typedef short uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long long uint64_t;

typedef int intptr_t;
typedef unsigned uintptr_t;

stdarg

这个库主要是服务于变参函数,因为我们想实现printf,那关于变参的处理,是需要一些辅助工具的。

#include <stdint.h>
#include <stddef.h>

typedef uint8_t *va_list;

#define va_start(varg_ptr, last_val) (varg_ptr = (uint8_t *)(&last_val + 1))
#define va_arg(varg_ptr, type) (varg_ptr += sizeof(type), *((type *)varg_ptr - 1))
#define va_end(varg_ptr) (varg_ptr = NULL)

如果读者对这几个宏有疑惑的话,那我们可以换一个思路来考虑。所谓变参,其实就是从C语言编译器的层面上不限制函数参数罢了,但读取参数的方式是没变的,都是从ebp+8开始是第一个参数,依次向上寻找。

所以,当我们确定了变参的头部以后,按照指针偏移向上寻找即可。所以va_list就是变参头部的指针,va_start用于确定变参头部的地址,而va_arg则是通过参数的类型来获取数据,并且进行指针偏移。最后的va_end是将指针清空。

稍后我们会利用它来实现printf

string

这里实现一些与字符串相关的工具,注意其实这些工具更适合用汇编直接来实现,但暂时这里先给出C语言实现的版本:

#include "include/string.h"
#include "include/stdint.h"

char *strcpy(char *dst, const char *src) {
  char *p = dst;
  while (*src != '\0') {
    *p++ = *src++;
  }
  return dst;
}

size_t strlen(const char *str) {
  size_t size = 0;
  for (const char *p = str; *p != '\0'; p++) {
    size++;
  }
  return size;
}

void *memcpy(void *dst, const void *src, size_t size) {
  uint8_t *p = (char *)dst;
  const uint8_t *q = (const uint8_t *)src;
  for (int i = 0; i < size; i++) {
    p[i] = q[i];
  }
  return dst;
}

void *memset(void *dst, int ch, size_t size) {
  uint8_t *p = dst;
  for (long i = 0; i < size; i++) {
    p[i] = (uint8_t)ch;
  }
  return dst;
}

stdio

最后,咱们在stdio上实现sprintfprintf,这里我们仅实现基本的格式符,复杂的(如%0.3f)暂时不考虑,如果读者感兴趣可以自行实现。代码如下:

#include "include/stdio.h"
#include "include/string.h"

extern void SetVMem(long addr, unsigned char data);

#define STDOUT_BUF_SIZE 1024

// 定义光标信息
typedef struct {
  long offset; // 暂时只需要一个偏移量
} CursorInfo;

static CursorInfo g_cursor_info = {0}; // 全局变量,保存光标信息

int putchar(int ch) {
  if (ch == '\n') { // 处理换行
    g_cursor_info.offset += 80 * 2; // 一行是80字符
    g_cursor_info.offset -= ((g_cursor_info.offset / 2) % 80) * 2; // 回到行首
  } else {
    SetVMem(g_cursor_info.offset++, (unsigned char)ch);
    SetVMem(g_cursor_info.offset++, 0x0f);
  }
  return ch;
}

int puts(const char *str) {
  // 处理C字符串,需要向后找到0结尾,逐一调用putchar
  for (const char *p = str; *p != '0'; p++) {
    putchar(*p);
  }
  return 0;
}

static size_t int_to_string(char *res, int i, uint8_t base) {
  if (base > 16 || base <= 1) {
    return 0;
  }
  if (i == 0) {
    res[0] = '0';
    return 1;
  }
  int size = 0;
  if (i < 0) {
    res[0] = '-';
    i *= -1;
    size++;
  }

  int quo = i / base;
  int rem = i % base;
  // 利用函数递归栈逆向结果
  if (quo != 0) {
    size += int_to_string(res + size, quo, base);
  }
  
  if (rem >= 0 && rem <= 9) {
    res[size] = (char)rem + '0';
    size++;
  } else if (rem <= 15) {
    res[size] = (char)rem - 10 + 'a';
    size++;
  }

  return size;
}

static size_t uint_to_string(char *res, unsigned i, uint8_t base) {
  if (base > 16 || base <= 1) {
    return 0;
  }
  if (i == 0) {
    res[0] = '0';
    return 1;
  }
  int size = 0;

  int quo = i / base;
  int rem = i % base;
  // 利用函数递归栈逆向结果
  if (quo != 0) {
    size += int_to_string(res, quo, base);
  }
  
  if (rem >= 0 && rem <= 9) {
    res[size] = (char)rem + '0';
    size++;
  } else if (rem <= 15) {
    res[size] = (char)rem - 10 + 'a';
    size++;
  }

  return size;
}

int vsprintf(char *str, const char *fmt, va_list li) {
  const char *p_src = fmt;
  char *p_dst = str;
  while (*p_src != '\0') {
    if (*p_src == '%') {
      p_src++;
      switch (*p_src++) {
        case '%':
          *p_dst++ = '%';
          break;

        case 'd':
          p_dst += int_to_string(p_dst, va_arg(li, int), 10);
          break;
        
        case 'u':
          p_dst += uint_to_string(p_dst, va_arg(li, unsigned), 10);
          break;
        
        case 'x':
          p_dst += uint_to_string(p_dst, va_arg(li, unsigned), 16);
          break;

        case 'o':
          p_dst += uint_to_string(p_dst, va_arg(li, unsigned), 8);
          break;

        case 'c':
          *p_dst++ = (char)va_arg(li, int); // 4字节对齐
          break;

        case 's':
          const char *str = va_arg(li, const char *);
          strcpy(p_dst, str);
          p_dst += strlen(str);
      }
    } else {
      *p_dst++ = *p_src++;
    }
  }
  return p_dst - str;
}

int sprintf(char *str, const char *fmt, ...) {
  va_list li;
  va_start(li, fmt);
  int ret = vsprintf(str, fmt, li);
  va_end(li);
  return ret;
}

int vprintf(const char *fmt, va_list li) {
  char buf[STDOUT_BUF_SIZE];
  memset(buf, 0, sizeof(buf));
  int ret = vsprintf(buf, fmt, li);
  if (ret < 0) {
    return ret;
  }
  for (const char *p = buf; *p != 0; p++) {
    putchar(*p);
  }
  return ret;
}

int printf(const char *fmt, ...) {
  va_list li;
  va_start(li, fmt);
  int ret = vprintf(fmt, li);
  va_end(li);
  return ret;
}

看一看效果

在构建之前还有一个问题要注意,咱们之前的代码在MBR里只读取了2个扇区,随着Kernel的逐渐增大,这个大小可能很快就超了,所以咱们要去MBR里改一下,让它多读几个扇区:

; LBA28模式,逻辑扇区号28位,从0x00000000xFFFFFFF
; 设置读取扇区的数量
mov dx, 0x01f2
mov al, 12 ; 读取连续的几个扇区,每读取一个al就会减1
out dx, al
; 设置起始扇区号,28位需要拆开
mov dx, 0x01f3
mov al, 0x02 ; 从第2个扇区开始读(1起始,0留空),扇区号0~7位
out dx, al
mov dx, 0x01f4 ; 扇区号8~15位
mov al, 0
out dx, al
mov dx, 0x01f5 ; 扇区号16~23位
mov al, 0
out dx, al
mov dx, 0x01f6
mov al, 111_0_0000b ;4位是扇区号24~27位,第4位是主从盘(01从),高3位表示磁盘模式(111表示LBA)
; 配置命令
mov dx, 0x01f7
mov al, 0x20 ; 0x20命令表示读盘
out dx, al

wait_finish:
	; 检测状态,是否读取完毕
	mov dx, 0x01f7
	in al, dx ; 通过该端口读取状态数据
	and al, 1000_1000b ; 保留第7位和第3位
	cmp al, 0000_1000b ; 要检测第7位为0(表示不在忙碌状态)和第3位是否是1(表示已经读取完毕)
	jne wait_finish ; 如果不满足则循环等待
	
	; 从端口加载数据到内存
	mov cx, 1024 * 12 / 2 ; 一共要读的字节除以2(表示次数,因为每次会读2字节所以要除以2)
	mov dx, 0x01f0
	mov ax, 0x0800
	mov ds, ax
	xor bx, bx ; [ds:bx] = 0x08000
read:
	in ax, dx ; 16位端口,所以要用16位寄存器
	mov [bx], ax
	add bx, 2 ; 因为ax是16位,所以一次会写2字节
	loop read

另一个就是,由于gcc在编译时会自动去寻找系统自带的C头文件,可能会造成编译时函数重复定义的报错,因此,我们还需要在每一个C文件的编译指令加一个-fno-buildin参数,例如:

entry.o: entry.c ../libc/include/stdio.h
	x86_64-elf-gcc -c -m32 -march=i386 -fno-builtin -I../libc/include entry.c -o entry.o

最后我们在Entry()中调用printf

#include <stdio.h>

void Entry() {
  const char *data = "ABC123~~";
  int a = 6;
  printf("Hello, World!\n%s\n%d", data, a);
}

运行结果如下:
运行结果1

至此,咱们已经基本实现从裸机启动开始运行了一个相对完整的C程序了。那是不是在这个基础上链接个C++程序就全剧终了呢?放心!自然不会。虽然说C++也可以很轻松链接到目前的工程上,但笔者希望能带领大家更近一步,比如进入64位模式,比如显示图像(而不是纯文本)。

本节的实例工程会上传至附件(12-2),后面章节我们还会继续探索。

小结

本篇将工程文件重新整理,并补充了一些C的库函数似的工程基本可用。下一篇将会介绍图形模式,以及在这个模式下的代码改造。

从裸机启动开始运行一个C++程序(十三)

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

borehole打洞哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值