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

news2024/12/21 16:35:14

前序文章请看:
从裸机启动开始运行一个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的库函数似的工程基本可用。下一篇将会介绍图形模式,以及在这个模式下的代码改造。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1121340.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot连接MySQL密码错误,报错:Access denied for user

记&#xff1a;一次连接MySQL报密码错误&#xff0c;Access denied for user 检查步骤&#xff1a; 核对用户和密码是否正确&#xff0c;用工具登陆试下。如果配置文件是yml格式&#xff0c;配置密码是123456这种纯数字&#xff0c;记得加上单/双引号。检查云上数据库配置&am…

使用rna-seq定量软件salmon运行index步骤遇到的一个问题(计算集群slurm)

salmon 帮助文档 https://salmon.readthedocs.io/en/latest/building.html#installation github主页 https://github.com/COMBINE-lab/salmon 我最开始是直接使用conda安装的 v1.4 首先第一步是对参考转录组进行索引&#xff0c;命令 salmon index -t pome.fa -i transcr…

postgresql14-用户与角色(二)

介绍 查看 SELECT rolname FROM pg_roles;postgres是系统初始化时默认创建的角色&#xff0c;为超级管理员。 \duList of rolesRole name | Attributes | Member of ------------------------------------------------------…

python 之计算矩阵乘法

文章目录 总的介绍例子 总的介绍 np.matmul 是NumPy库中的矩阵乘法函数&#xff0c;用于执行矩阵乘法操作。矩阵乘法是线性代数中的一种常见操作&#xff0c;用于将两个矩阵相乘以生成新的矩阵。在神经网络、机器学习和科学计算中&#xff0c;矩阵乘法经常用于变换和组合数据。…

简单宿舍管理系统(springboot+vue)

简单宿舍管理系统&#xff08;springbootvue&#xff09; 1.创建项目1.前端2.数据库3.后端 2.登陆1.前端1.准备工作2.登陆组件3.配置 2.后端1.链接数据库2.创建用户实体类3.数据操作持久层1.配置2.内容3.测试 4.中间业务层1.异常2.业务实现3.测试 5.响应前端控制层 3.前后对接4…

散列表:Word文档中的单词拼写检查功能是如何实现的?

文章来源于极客时间前google工程师−王争专栏。 一旦我们在Word里输入一个错误的英文单词&#xff0c;它就会用标红的方式提示“编写错误”。Word的这个单词拼写检查功能&#xff0c;虽然很小但却非常实用。这个功能是如何实现的&#xff1f; 散列别&#xff08;Hash Table&am…

linux复习笔记02(小滴课堂)

linux下输入输出错误重定向&#xff1a; 输入重定向&#xff1a;< 一个大于号是进行了覆盖。 两个大于号是追加。 输出重定向可以用于以后日志打印。 错误重定向&#xff1a; 错误重定向是不把信息打印到屏幕上而是打印到指定文件中去&#xff1a; 输出重定向其实是用的1…

【论文阅读】(2023TPAMI)PCRLv2

目录 AbstractMethodMethodnsU-Net中的特征金字塔多尺度像素恢复多尺度特征比较从多剪切到下剪切训练目标 总结 Abstract 现有方法及其缺点&#xff1a;最近的SSL方法大多是对比学习方法&#xff0c;它的目标是通过比较不同图像视图来保留潜在表示中的不变合判别语义&#xff…

可视化数学分析软件 MATLAB R2021b mac中文版软件介绍

MATLAB R2021b mac作为数学类科技应用软件中首屈一指的商业数学软件&#xff0c;可以帮助您进行矩阵运算、绘制函数和数据、实现算法、创建用户界面、连接其他编程语言的程序等,主要应用于工程计算、控制设计、信号处理与通讯、图像处理、信号检测、金融建模设计与分析等领域。…

算法刷题-链表

算法刷题-链表 203. 移除链表元素 给你一个链表的头节点 head 和一个整数 val &#xff0c;请你删除链表中所有满足 Node.val val 的节点&#xff0c;并返回 新的头节点 。 示例 1&#xff1a; 输入&#xff1a;head [1,2,6,3,4,5,6], val 6 输出&#xff1a;[1,2,3,4,5]…

小成代码路的错误

文章目录 计算长度使用左闭右开的方法getline(解决输入空格问题和给一个输入的界限&#xff09;stoi(字符串转整形&#xff09;stoi&#xff08;整型转字符串型&#xff09;reverse&#xff08;反转函数&#xff09;将字符串立志静成员变量在类外初始化&#xff1b;但是有特例&…

FreeSWITCH 1.10.10 简单图形化界面12 - 注册IMS

FreeSWITCH 1.10.10 简单图形化界面12 - 注册IMS 0、 界面预览1、IMS注册-SIP中继基本设置界面2、IMS注册-SIP中继呼叫设置3、IMS中继-代理设置界面4、IMS注册-SIP中继状态界面5、IMS注册-SIP中继详细状态界面6、IMS注册-SIP中继代拨号码优先界面 FreeSWITCH界面安装参考&#…

Qt planeGame day10

Qt planeGame day10 Game基本框架 qt中没有现成的游戏框架可以用&#xff0c;我们需要自己搭框架首先创建一个QGame类作为框架&#xff0c;这个基本框架里面应该有如下功能&#xff1a;游戏初始化 void init(const QSize& siez,const QString& title);游戏反初始化(…

系统报错“由于找不到vcomp140.dll无法继续执行代码”的解决方案

在我们日常使用电脑的过程中&#xff0c;可能会遇到一些错误提示&#xff0c;其中之一就是“找不到vcomp140.dll”。这个错误可能让许多用户感到困扰&#xff0c;因为它可能影响到我们的电脑使用。那么&#xff0c;vcomp140.dll是什么意思&#xff1f;当我们遇到这个问题时&…

学习笔记2——Nosql

学习笔记系列开头惯例发布一些寻亲消息 链接&#xff1a;https://baobeihuijia.com/bbhj/contents/3/194205.html 跟学链接 跟学视频链接&#xff1a;https://www.bilibili.com/video/BV1S54y1R7SB/?spm_id_from333.999.0.0 &#xff08;建议有java基础的同学学习或者一直…

发挥设计模式单例模式的力量:从技术到社会的转变

文章目录 &#x1f31f; 如何将设计模式单例模式运用到社会当中&#x1f34a; 什么是单例模式&#x1f34a; 单例模式在现实生活中的应用&#x1f389; 数据库连接池&#x1f389; 日志管理器&#x1f389; 系统配置中心 &#x1f34a; 如何将单例模式应用于社会中&#x1f389…

【Unity程序技巧】公共Update管理器

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;Uni…

hugo-stack for github

静态博客框架jekyll、hexo和hugo三者之间的区别与差异 博客生成器? 全名为静态网站生成器&#xff0c; 可在任意拥有主机功能的环境下寄存(托管)可直接配合域名进行全球访问 劣势: 每次更新网页必须重新生成整个网站编译速度&#xff08;单位&#xff1a;秒&#xff09; Jek…

Win下Eclipse安装

eclipse官网https://www.eclipse.org/downloads/ 软件介绍&#xff1a; Eclipse 是一个开放源代码的、基于 Java 的可扩展开发平台。Eclipse 是 Java 的集成开发环境&#xff08;IDE&#xff09;&#xff0c;当然 Eclipse 也可以作为其他开发语言的集成开发环境&#xff0c;如…

为什么使用KT6368A蓝牙芯片用app连接,基本都在5分钟左右后断开

为什么我们自己的板子用KT6368A蓝牙芯片&#xff0c;用app连接&#xff0c;基本都是在5分钟左右后 断掉或者断开&#xff0c;什么原因。参考的原理图&#xff0c;是官方的图纸如下 遇到这样的问题&#xff0c;根据我们的经验&#xff0c;大概率是硬件问题&#xff0c;分析方法如…