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

前序文章请看:
从裸机启动开始运行一个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)中,供读者参考。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

lightdb-ignore_row_on_dupkey_index

LightDB 支持 ignore_row_on_dupkey_index hint LightDB 从23.4 开始支持oracle的 ignore_row_on_dupkey_index hint&#xff0c; 这个hint是用来忽略唯一键冲突的。类似与mysql的 insert ignore。 语法如下&#xff1a; 在LightDB中ignore_row_on_dupkey_index的效果等同于o…

大坝安全监测的内容及作用

大坝安全监测是指对大坝水雨情沉降、倾斜、渗压以及大坝形状特征有效地进行监测&#xff0c;及时发现潜在的安全隐患和异常情况&#xff0c;以便大坝管理人员能够做出科学决策&#xff0c;以确保大坝安全稳定运行。 大坝安全监测的主要内容 1.表面位移监测&#xff1a;监测大坝…

Vue基础入门(三):Vue3的使用

Vue3的使用 一、首页案例修改 修改首页的信息&#xff1a;是在之前介绍的HelloWorld.vue文件中进行内容的修改。 页面展示效果&#xff1a; 此时就看到了我们新添加的文字了&#xff01; 同样的我们开发代码的时候只需要修改了项目中的内容然后保存就会自动刷新的浏览器&…

接口测试:Jmeter和Postman测试方法对比

前阶段做了一个小调查&#xff0c;发现软件测试行业做功能测试和接口测试的人相对比较多。在测试工作中&#xff0c;有高手&#xff0c;自然也会有小白&#xff0c;但有一点我们无法否认&#xff0c;就是每一个高手都是从小白开始的&#xff0c;所以今天我们就来谈谈一大部分人…

OpenSearch向量检索和大模型方案深度解读

大家好&#xff0c;我叫邢少敏&#xff0c;目前负责阿里云开放搜索OpenSearch的研发&#xff0c;很高兴在此跟大家分享OpenSearch在向量检索和大模型方面做的一些工作。 基于向量检索的分布式智能搜索引擎 通常&#xff0c;数据大致可以分为结构化数据和非结构化数据两种类型…

“2024上海智博会、2024北京智博会”双展联动,3月上海,6月北京

“2024上海智博会、2024北京智博会”双展联动&#xff0c;将分别于3月和6月在上海和北京举办。这两个展会旨在充分展示智慧城市、人工智能、物联网、大数据、软件等新兴行业的最新产品和技术。 作为中国最具影响力和创新力的智能科技展会&#xff0c;上海智博会和北京智博会吸引…

ArkTS-属性动画和显式动画

属性动画 组件的某些通用属性变化时&#xff0c;可以通过属性动画实现渐变过渡效果&#xff0c;提升用户体验。支持的属性包括width、height、backgroundColor、opacity、scale、rotate、translate等。 名称参数类型必填描述durationnumber否设置动画时长。默认值&#xff1a;1…

ArkUI 如何将$r(’app.string.xxx‘) 转成string字符串

一、正常引用字符串资源文件内容 在 ArkUI 中&#xff0c;string.json 中的字符串资源正常情况下使用如下方式引用&#xff1a; Entry Component struct LoginPage {build() {Column() {Text($r(app.string.title))}}}二、资源转string类型 上面的代码没问题是因为 Text(con…

探究Kafka原理-7.exactly once semantics 和 性能测试

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱吃芝士的土豆倪&#xff0c;24届校招生Java选手&#xff0c;很高兴认识大家&#x1f4d5;系列专栏&#xff1a;Spring源码、JUC源码、Kafka原理&#x1f525;如果感觉博主的文章还不错的话&#xff0c;请&#x1f44…

盖茨表示GPT-5不会比GPT-4有太大改进;Intro to Large Language Models

&#x1f989; AI新闻 &#x1f680; 盖茨表示GPT-5不会比GPT-4有太大改进 摘要&#xff1a;比尔盖茨在与德国《商报》的采访中透露&#xff0c;虽然OpenAI内部有人相信GPT-5会优于GPT-4&#xff0c;但他认为目前的生成式人工智能已经达到极限。盖茨对GPT-5未来的发展并不乐观…

Vue基础入门(四):Vue3快速开发模板

快速开发Vue的解决方案 ​ Vue 的开发需要的 node 环境&#xff0c;其实上在开发的过程中会遇到一些你想不到的问题&#xff0c;比如 npm工具的版本和 node 环境不匹配&#xff08;你把其他项目导入到自己的环境&#xff09; ​ vue-element-admin&#xff08;是一个官方提供…

Ubuntu系统CLion安装

Ubuntu系统CLion安装 pycharm 同理。 参考官网安装过程&#xff1a;官网安装过程 下载linux tar.gz包 # 解压 sudo tar -xzvf CLion-*.tar.gz -C /opt/ sh /opt/clion-*/bin/clion.sh其中第二个命令是启动CLion命令 clion安装完以后&#xff0c;不会在桌面或者菜单栏建立图…

Containerd Container管理功能解析

Containerd Container管理功能解析 container是containerd的一个核心功能&#xff0c;用于创建和管理容器的基本信息。 本篇containerd版本为v1.7.9。 更多文章访问 https://www.cyisme.top 本文从ctr c create命令出发&#xff0c;分析containerd的容器及镜像管理相关功能。 …

[英语学习][3][Word Power Made Easy]的精读与翻译优化

[序言] 这次翻译校验, 难度有点大, 原版中英翻译已出现了严重地偏差. 昨晚11点开始阅读如下段落, 花费了1个小时也没有理解原作者的核心表达, 索性睡觉了. 今早学习完朗文单词之后, 9点半开始继续揣摩. 竟然弄到了中午11点30, 终于明白原作者要表达的意思了. 废话不多说&#x…

Selenium 学习(0.14)——软件测试之测试用例设计方法——因果图法2【基本步骤及案例】

1、因果图法的基本步骤 2、案例分析 1&#xff09;分析原因和结果 2&#xff09;关联原因和结果 投入1元5角或2元&#xff0c;按下“可乐”&#xff0c;送出“可乐”【暂时忽略找零】 投入2元&#xff0c;按下“可乐”或“雪碧”。找零5角&#xff0c;送出“可乐”或“雪…

大量用户弃用5G网络,四大运营商血亏? 喜闻乐见

家人们&#xff0c;老百姓讨厌5G网络&#xff0c;不是一天两天的事情了&#xff0c;有人认为5G网络是个坑&#xff0c;我们就应该永远用4G网络才对&#xff0c;国家为何要折腾这事&#xff1f;肯定是闲的。 正是这种思维的蔓延&#xff0c;导致了大量用户弃用5G网络。这事对四大…

搭建Angular并引入NG-ZORRO组件库

作者&#xff1a;baekpcyyy&#x1f41f; 1.安装node.js 注&#xff1a;安装 16.0 或更高版本的 Node.js node官网&#xff1a;https://nodejs.org/en 2.进入angular官网 https://angular.cn/guide/setup-local 新建一个文件夹 vsc打开 打开终端 1.首先安装angular手脚架…

高级/进阶”算法和数据结构书籍推荐

“高级/进阶”算法和数据结构书籍推荐《高级算法和数据结构》 高级算法和数据结构 为什么要选择本书 谈及为什么需要花时间学算法&#xff0c;我至少可以列举出三个很好的理由。 (1)性能&#xff1a;选择正确的算法可以显著提升应用程序的速度。仅就搜索来说&#xff0c;用二…

Python基础语法之学习input()函数

Python基础语法之学习input函数 前言一、代码二、效果 前言 一、代码 # 默认是字符串类型 number input("请输入一个数字&#xff1a;") print("输入的数字是",number)二、效果 没有人可以阻止你成为自己想成为的人&#xff0c;只有你自己才能放弃梦想。…

WinMerge使用教程,WinMerge下载

一、下载 官方下载 WinMerge - You will see the difference… 官方地址&#xff1a;https://winmerge.org/ 阿里云盘下载 文件内容对比工具WinMerge2.16.25.25 https://www.alipan.com/s/r7MzudB235x 点击链接保存&#xff0c;或者复制本段内容&#xff0c;打开「阿里云盘…