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

news2024/9/24 9:21:13

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

艰辛的C++程序

趁热打铁

上一章的最后咱们已经成功把C++文件链接进Kernel了,趁热打铁,我们用C++语法来实现绘制图形的功能,比如我们可以将绘制点和绘制矩形的方法封装成类,通过调用SetVMem进行操作。代码如下:

extern "C" { // 由于这些库都是C方式的,因此需要额外声明
  #include <stdint.h>
  extern void SetVMem(long addr, uint8_t data); 
}

constexpr int screen_width = 320;
constexpr int screen_length = 200;

class Point {
 public:
  Point(int x, int y);
  ~Point() = default;
  void Draw(uint8_t color) const;

 private:
  int x_, y_;
};

Point::Point(int x, int y): x_(x), y_(y) {}

void Point::Draw(uint8_t color) const {
  SetVMem(y_ * screen_width + x_, color);
}

class Rect {
 public:
  Rect(int x, int y, int width, int length);
  ~Rect() = default;
  void Draw(uint8_t color) const;

 private:
  int x_, y_, width_, length_;
};

Rect::Rect(int x, int y, int width, int length): x_(x), y_(y), width_(width), length_(length) {}

void Rect::Draw(uint8_t color) const {
  for (int i = 0; i < width_; i++) {
    for (int j = 0; j < length_; j++) {
      Point{x_ + i, y_ + j}.Draw(color);
    }
  }
}

extern "C"
int main() {
  Rect{10, 10, 50, 30}.Draw(0x26);
  return 0;
}

运行效果

目前功能都没有问题,接下来我们要做一下工程源码的整理。

C库的C++改造

由于我们要将C库引入到C++代码中,所以如果都是在C++中显式使用extern "C"就会很麻烦,因此好的做法是,把这种差别体现在头文件中,无论是C语言还是C++都可以直接使用。

编译器用于区别C还是C++源码的方法是通过一个编译宏__cplusplus,这个宏同时还表示了C++版本。因此,我们在头文件中进行判断,如果含有这个宏,就自动添加extern "C",否则不添加。也就是这样:

#ifdef __cplusplus
extern "C" {
#endif

// 这里是头文件的实际内容
// ...

#ifdef __cplusplus
}
#endif

我们将C库中的所有头文件都按这种方式改造,并且把SetVMem函数也提供在stdio.h中。这样,对于main.cpp来说,只需要正常引入头文件即可。

将图形绘制相关代码独立

我们把挤在main.cpp中的图形绘制相关代码单独抽出去,创建graphic_ui.hppgraphic_ui.cpp文件。

// graphic_ui.hpp
#pragma once
#include <stdint.h>

namespace ui {

constexpr int screen_width = 320;
constexpr int screen_length = 200;

class Point {
 public:
  Point(int x, int y);
  ~Point() = default;
  void Draw(uint8_t color) const;

 private:
  int x_, y_;
};

class Rect {
 public:
  Rect(int x, int y, int width, int length);
  ~Rect() = default;
  void Draw(uint8_t color) const;

 private:
  int x_, y_, width_, length_;
};

}
// graphic_ui.cpp
#include "graphic_ui.hpp"
#include <stdio.h>

namespace ui {

Point::Point(int x, int y): x_(x), y_(y) {}

void Point::Draw(uint8_t color) const {
  SetVMem(y_ * screen_width + x_, color);
}

Rect::Rect(int x, int y, int width, int length): x_(x), y_(y), width_(width), length_(length) {}

void Rect::Draw(uint8_t color) const {
  for (int i = 0; i < width_; i++) {
    for (int j = 0; j < length_; j++) {
      Point{x_ + i, y_ + j}.Draw(color);
    }
  }
}

}

同时,加上对应的makefile

.PHONY: all
all: kernel_final.bin

kernel.o: kernel.nas
	nasm kernel.nas -f elf64 -o kernel.o

graphic_ui.o: graphic_ui.cpp graphic_ui.hpp
	x86_64-elf-g++ -c -std=c++17 -m64 -march=x86-64 -fno-builtin -I../libc/include graphic_ui.cpp -o graphic_ui.o -Wall -Werror -Wextra

main.o: main.cpp graphic_ui.hpp
	x86_64-elf-g++ -c -std=c++17 -m64 -march=x86-64 -fno-builtin -I../libc/include main.cpp -o main.o -Wall -Werror -Wextra

entry.o: entry.c ../libc/include/stdio.h
# 需要用-I制定头文件扫描位置
	x86_64-elf-gcc -c -m64 -march=x86-64 -fno-builtin -I../libc/include entry.c -o entry.o -Wall -Werror -Wextra

../libc/libc.a:
	pushd ../libc && $(MAKE) clean && $(MAKE)  libc.a && popd

kernel_final.out: kernel.o entry.o main.o graphic_ui.o ../libc/libc.a 
# 需要用-L指定静态链接库位置
# -lc表示链接libc.a
# 注意kernel.o要放在第一个
	x86_64-elf-ld -m elf_x86_64 -Ttext=0x8000 kernel.o entry.o main.o graphic_ui.o -L../libc -lc -o kernel_final.out

kernel_final.bin: kernel_final.out
	x86_64-elf-objcopy -I elf64-x86-64 -S -R ".eh_frame" -R ".comment" -O binary kernel_final.out kernel_final.bin

.PHONY: clean
clean:
	-rm -f .DS_Store
	-rm -f *.bin 
	-rm -f *.o
	-rm -f *.out

主函数则改造为:

#include <stdint.h> // 改造后的头文件可以直接引用
#include "graphic_ui.hpp"

extern "C"
int main() {
  ui::Rect{10, 10, 50, 30}.Draw(0x26);
  return 0;
}

至此的项目源码放在附件(15-1)中,读者可自行验证。

绘制圆

已经有了绘制点和矩形的类了,我们想再添加一个绘制圆形的类。圆需要圆心和半径来确定,而圆形就是一个点,因此这里我们正好可以测试一下类的组合。代码如下:

// graphic_ui.hpp
class Circle {
 public:
  Circle(const Point &center, int radium);
  ~Circle() = default;
  void Draw(uint8_t color) const;

 private:
  Point center_;
  int radium_;
};

// graphic_ui.cpp
Circle::Circle(const Point &center, int radium): center_(center), radium_(radium) {}

void Circle::Draw(uint8_t color) const {
  // 采用点阵扫描的方法,沿着x轴,从(c.x - r, c.y)开始,一直绘制到(c.x + r, c.y)
  // 中间横坐标每增加1,就计算当前横坐标上,符合(x-c.x)²+(y-c.y)²≤r²的纵坐标值,并绘制颜色
  for (int x = center_.x - radium_; x <= center_.x + radium_; i++) {
    // y = c.y±√(r²-(x-c.x)²)
    int y1 = center_.y - ::sqrt(radium_ * radium_ - (x - center_) * (x - center_));
    int y2 = center_.y + ::sqrt(radium_ * radium_ - (x - center_) * (x - center_));
    for (int y = y2; y < y1; y++) {
        Point{x, y}.Draw(color);
    }
  }
}

由于这里需要开平方的能力,因此我们在C库中添加math.hmath.c,同时实现sqrt函数:

// math.h
#ifdef __cplusplus
extern "C" {
#endif
#include "stdint.h"

int abs(int n);
int sqrt(int n);

#ifdef __cplusplus
}
#endif
// math.c
#include "include/math.h"

int abs(int n) {
  if (n < 0) {return -n;}
  return n;
}

int sqrt(int n) {
  if (n < 0) {return 0;}
  // 由于是整数,直接暴力尝试
  for (int i = 0; i < n; i++) {
    if (i * i <= n && (i + 1) * (i + 1) >= n) {
        return i;
    }
  }
  
  return 0;
}

之所以这里用整型,主要是当前没有配置浮点型的相关运算,在Intel体系中,浮点运算是又x87部件运行的,所以这部分都有单独的运行指令,而我们没有做相关配置,所以只要程序中出现浮点型,就会执行失败。不过当前需求下整型也完全够用了,所以这里先用整型。

主函数中绘制圆看看结果:

#include <stdint.h>
#include "graphic_ui.hpp"

extern "C"
int main() {
  ui::Circle{{100, 100}, 80}.Draw(0x23);

  return 0;
}

效果如下:
运行结果
圆已经可以绘制出来了,但这个返回值为什么是35呢?看来上了64位以后,变参的获取方式也存在了一些问题。之前我们在stdarg.h中是这样定义的:

#define va_start(varg_ptr, last_val) (varg_ptr = ((uint8_t *)&last_val + sizeof(last_val)))

在64位环境下这个写法是有问题的,原因很简单,我们之前提到过,因为在64位环境下,函数参数并不是全部压栈的,而是优先进入寄存器。虽然,为了解析这些参数,编译器还是会把它们重新入栈,但有一个严重的问题,就是last_val和真实变参并不是连续的。

比如,我们定义如下变参函数:

void Demo(int a, ...) {}

汇编后是:

Demo(int, ...):
        push    rbp
        mov     rbp, rsp
        sub     rsp, 72
        mov     DWORD PTR [rbp-180], edi ; a
        mov     QWORD PTR [rbp-168], rsi ; arg1
        mov     QWORD PTR [rbp-160], rdx
        mov     QWORD PTR [rbp-152], rcx
        mov     QWORD PTR [rbp-144], r8
        mov     QWORD PTR [rbp-136], r9
        test    al, al
        je      .L3
        movaps  XMMWORD PTR [rbp-128], xmm0
        movaps  XMMWORD PTR [rbp-112], xmm1
        movaps  XMMWORD PTR [rbp-96], xmm2
        movaps  XMMWORD PTR [rbp-80], xmm3
        movaps  XMMWORD PTR [rbp-64], xmm4
        movaps  XMMWORD PTR [rbp-48], xmm5
        movaps  XMMWORD PTR [rbp-32], xmm6
        movaps  XMMWORD PTR [rbp-16], xmm7
.L3:
        nop
        leave
        ret

可以看到a与第一个变参之间并不是差64位。而且,这个值会随着Demo中局部变量的增加而改变。

因此,在64位环境下,我们不能在通过简单的宏定义来完成,编译器会把变参从寄存器中,先取出来放在栈内的某一个空间(比如上例中的rbp-168),然后当调用va_arg时,再把指针指向对应的参数位置。

由于在这种场景下,语言标准并没有定义这些参数从寄存器中取出来后如何布局,因此这些行为完全由编译器来决定。编译器自身实现了这些变参的解析功能,所以,我们直接调用编译器的内建函数:

// 通过编译器内建功能来完成
typedef __builtin_va_list va_list;
#define va_start(v, l) __builtin_va_start(v, l)
#define va_arg(v, t) __builtin_va_arg(v, t)
#define va_end(v) __builtin_va_end(v)

而具体的__builtin方法的实现,交由编辑器即可。

所以,改造完这个以后我们再看看运行结果:
运行效果

这个小bug也解决了。

至此,工程源码将会在附件(15-2)中,供读者参考。

虚函数链接问题

在编写图形渲染类的时候大家应该能够发现一个问题,就是Point,Rect,Circle都属于「图形」,并且都实现了用于渲染的Draw方法,因此,按照OOP设计,它们应当同属一个父类。

因此我们抽象一个Shape父类,将Draw方法改为虚函数。代码如下:

#pragma once
#include <stdint.h>

namespace ui {

constexpr int screen_width = 320;
constexpr int screen_length = 200;

class Shape {
 public:
  virtual void Draw(uint8_t color) const = 0;
};

class Point : public Shape {
 public:
  Point(int x, int y);
  ~Point() = default;
  void Draw(uint8_t color) const override;
  int x() const {return x_;}
  int y() const {return y_;}

 private:
  int x_, y_;
};

class Rect : public Shape {
 public:
  Rect(int x, int y, int width, int length);
  ~Rect() = default;
  void Draw(uint8_t color) const override;

 private:
  int x_, y_, width_, length_;
};

class Circle : public Shape {
 public:
  Circle(const Point &center, int radium);
  ~Circle() = default;
  void Draw(uint8_t color) const override;

 private:
  Point center_;
  int radium_;
};

}

不过这时,构建的时候就会发现以下报错:

x86_64-elf-ld: graphic_ui.o:(.rodata._ZTIN2ui6CircleE[_ZTIN2ui6CircleE]+0x0): undefined reference to `vtable for __cxxabiv1::__si_class_type_info'
x86_64-elf-ld: graphic_ui.o:(.rodata._ZTIN2ui4RectE[_ZTIN2ui4RectE]+0x0): undefined reference to `vtable for __cxxabiv1::__si_class_type_info'
x86_64-elf-ld: graphic_ui.o:(.rodata._ZTIN2ui5PointE[_ZTIN2ui5PointE]+0x0): undefined reference to `vtable for __cxxabiv1::__si_class_type_info'
x86_64-elf-ld: graphic_ui.o:(.rodata._ZTIN2ui5ShapeE[_ZTIN2ui5ShapeE]+0x0): undefined reference to `vtable for __cxxabiv1::__class_type_info'

报错是链接阶段的,说是没有找到__cxxabiv1::__si_class_type_info__cxxabiv1::__class_type_info的虚函数表。那这又是个什么东西呢?

从命名上我们可以得知,这玩意属于「C++ ABI v1」,也就是Application Binary Interface,应用程序二进制接口。也就是说,这应当是OS为App所实现的通用接口。作为应用程序App,在构建时,会依赖操作系统提供的这些接口。

上面缺少的type_info相关信息,就是C++ App在运行时RTTI(Run-Time Type Identification)所使用的一些类型信息。

正常来说,ABI的实现都在libc++库中,由对应的OS来提供。但这件事奇怪的点就在于,我们当前就是内核程序,并不是App,谁来提供ABI呢?显然,也只有我们自己了。

不过既然我们目前并没有RTTI的需求,所以我们构造一个假的,只要能让链接器找到就好了。代码如下:

namespace __cxxabiv1 {
  struct __si_class_type_info {
    virtual void f() {} // 必须有一个虚函数,才能构建虚函数表
  } ins1; // 必须至少有一个对象实例,才能促使类型构建虚函数表

  struct __class_type_info {
    virtual void f() {}
  } ins2;
}

这样再重新构建,发现正常了,运行结果如下:
运行结果

当然,这只是目前需求的做法,如果你真的想继续使用C++的其他功能,那对应的ABI还是要好好实现的。这也是很多人说C++并不适合写内核,原因就在这,它并不像C那样纯粹,必须依赖很多额外的东西才能够正常构建,而在写内核的时候这些东西往往是缺失的。

目前的项目源码将会在附件(15-3)中,供读者参考。

小结

我们用了15篇的篇幅,从x86架构的裸机启动开始,成功运行了一个C++程序,并且是内核态的。

下一篇将会是完结篇,我们将会总结和归纳整个系列,还会列举通过这件事情我们可以分析出的C++的一些理念,以及笔者个人的心得体会。

本篇的实例将会在附件(demo_code_15)中,供读者参考。

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

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

相关文章

金字塔原理 读书笔记

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言第1篇 表达的逻辑第1章 为什么要用金字塔结构归类分组&#xff0c;将思想组织成金字塔自上而下表达&#xff0c;结论先行自下而上思考&#xff0c;总结概括 第2…

物理机虚拟化关键技术介绍

☞ ░ 前往老猿Python博客 ░ https://blog.csdn.net/LaoYuanPython 一、虚拟化原理 将多个“同质或异构”资源&#xff08;包括但不限于芯片、硬件、软件、应用&#xff09;形成一个资源池&#xff0c;对资源池进行抽象、解耦形成独立的“虚拟资源”&#xff0c;并实现虚拟资…

【链接MySQL】教你用VBA链接MySQL数据库

hi&#xff0c;大家好呀&#xff01; 之前呢&#xff0c;给大家分享过一个自制链接表管理器的文章&#xff0c;文章中有链接SQL Server数据库的代码&#xff0c;大家对这一段代码比较有兴趣&#xff0c;既然大家有兴趣&#xff0c;那我们今天就来讲一下链接数据库的代码。 这…

振南技术干货集:znFAT 硬刚日本的 FATFS 历险记(9)

注解目录 1、znFAT 的起源 1.1 源于论坛 &#xff08;那是一个论坛文化兴盛的年代。网友 DIY SDMP3 播放器激起了我的兴趣。&#xff09; 1.2 硬盘 MP3 推了我一把 &#xff08;“坤哥”的硬盘 MP3 播放器&#xff0c;让我深陷 FAT 文件系统不能自拔。&#xff09; 1.3 我…

Solidworks模型上色技巧以及增加快捷键快速打开和关闭“阴影效果和楼板反射”

Solidworks模型上色技巧 Chapter1 给Solidworks模型上色技巧设置外观的方法具体操作删除颜色的技巧这样操作&#xff1a; Chapter2 SOLIDWORKS小技巧 | SolidWorks装配体零件快速上色自动设置Chapter3 solidworks装配图如何去掉阴影&#xff1f;Solidworks2022中的阴影效果怎么…

5.24每日一题(判断原函数是否为周期函数的基本定理 )

原函数为周期函数的充要条件&#xff1a;f(x)在某区间上的积分为0

高中生分科考试--座位编排系统

这个系统是帮我一同学的哥哥的做的座位编排系统&#xff0c;他是某个学校的教育从事者 基本需求&#xff1a;就是能够根据他提供的各个分科班级同学的成绩单来选择相同分科的考场编排&#xff08;按成绩高低&#xff09;&#xff0c;同时输入相应的考场数&#xff0c;和每个考…

【算法萌新闯力扣】:旋转链表

力扣题目&#xff1a;旋转链表 开篇 今天是备战蓝桥杯的第25天和算法村开营第3天&#xff01;经过这3天的学习&#xff0c;感觉自己对链表的掌握程度大大地提升&#xff0c;尤其是在帮村里的同学讨论相关问题时。本篇文章&#xff0c;给大家带来一道旋转链表的题目&#xff0c…

【VROC】看Intel VROC如何给NVMe SSD做RAID

在当今对硬盘性能要求越来越高的环境中&#xff0c;SATA和SAS接口由于自身的限制&#xff0c;其性能很难突破600MiB/s的瓶颈。因此&#xff0c;对于需要更高底层硬件性能的行业&#xff0c;如数据库等&#xff0c;对NVMe盘的需求越来越迫切。然而&#xff0c;NVMe盘直通到CPU&a…

2005-2022年全球各国经济距离数据

2005-2022年全球各国经济制度距离数据 1、时间&#xff1a;2005-2022年 2、指标&#xff1a;国家、年份、经济距离1&#xff08;根据美国传统基金会世界经济自由度指数整理&#xff09;、经济距离2&#xff08;参考(Kogut&Singh,1988)构建经济制度距离&#xff09; 3、范…

dst-admin饥荒管理后台 RCE漏洞复现(CVE-2023-0646、CVE-2023-0647、CVE-2023-0649)

0x01 产品简介 dst-admin饥荒管理后台是qinming99个人开发者的一个用 Java 语言编写的 web 程序。 0x02 漏洞概述 dst-admin饥荒管理后台kickPlayer、cavesConsole、sendBroadcast等接口处配置不当&#xff0c;导致破解口令后的攻击者可以进行命令注入&#xff0c;获取服务器权…

第五节HarmonyOS ArkTS声明式开发范式

ArkTS声明式开发范式&#xff1a; 规范中各个内容说明如下&#xff1a; 装饰器 1、基本UI装饰器Entry、Component Entry 装饰struct&#xff0c;页面的入口。 Component 装饰struct&#xff0c;表示该struct具有基于组件的能力。 2、数据装饰器State、Prop、Link State…

【ShardingSphere专题】SpringBoot整合ShardingSphere(一、数据分片入门及实验)

目录 前言阅读对象笔记正文一、ShardingSphere介绍1.1 ShardingSphere-JDBC&#xff1a;代码级别1.2 ShardingSphere-Proxy&#xff1a;应用级别1.3 横向对比图 二、ShardingSphere之——数据分片2.1 基本介绍2.2 分片的形式2.2.1 垂直分片2.2.2 水平分片 2.3 数据分片核心概念…

Python实现性能自动化测试

一、思考❓❔ 1.什么是性能自动化测试? 性能 系统负载能力超负荷运行下的稳定性系统瓶颈自动化测试 使用程序代替手工提升测试效率性能自动化 使用代码模拟大批量用户让用户并发请求多页面多用户并发请求采集参数&#xff0c;统计系统负载能力生成报告 2.Python中的性能自动…

代码随想录算法训练营 ---第四十九天

前言&#xff1a; 今天是买卖股票的最佳时机系列&#xff0c;本系列之前在学习贪心思想时做过一些。 第一题&#xff1a; 简介&#xff1a; 本题在读题时我们要注意到几个细节 1.本题股票买卖只有一次。2.我们要在最低点买股票&#xff0c;在最高点卖股票。 我的思路&#…

基于Java SSM框架+Vue实现药品保健品购物网站项目【项目源码+论文说明】计算机毕业设计

基于java的SSM框架Vue实现药品保健品购物网站演示 摘要 随着社会的发展&#xff0c;社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。 ssm药源购物网站&#xff0c;主要的模块包括两个用户&#xff0c;管理员权限&#xff1a;用…

C++初阶--String类的使用

string类 在C语言中&#xff0c;我们总是用char* 的类型来创建一个变量&#xff0c;存储一个字符串&#xff1b;当我们想对它进行修改或者读写时&#xff0c;需要自我创建空间和使用string.h的库函数来进行操作它&#xff1b; 而在C中&#xff0c;C专门提供了一个头文件 stri…

揭秘论文开题报告写作技巧,全程无忧,附赠技术路线图模板!

最近不少学校开始让准毕业生撰写论文开题报告&#xff0c;如果是第一次接触学术论文的朋友&#xff0c;多少会卡在概念的理解上&#xff0c;就像题主说到的&#xff0c;开题报告中包含的各个部分&#xff0c;如研究目的、研究目标、研究内容等&#xff0c;容易让人眼花缭乱。 …

ChatGPT到底是如何运作?

自从2022年11月30日发布以来&#xff0c;ChatGPT一直占据着科技届的头条位置&#xff0c;随着苹果的创新能力下降&#xff0c;ChatGPT不断给大家带来震撼&#xff0c;2023年11月7日&#xff0c;首届OpenAI开发者大会在洛杉矶举行&#xff0c;业界普遍认为&#xff0c;OpenAI的开…

2021年12月 Scratch图形化(四级)真题解析#中国电子学会#全国青少年软件编程等级考试

Scratch等级考试(1~4级)全部真题・点这里 一、单选题(共15题,每题2分,共30分) 第1题 下图两个积木的值分别是? A:false true B:false false C:true true D:true false 答案:A 第2题 小猫和小狗是非常好的朋友,他们发明了一种加密方法:用两位数字代表字母。…