从裸机启动开始运行一个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.binkernel.o: kernel.nasnasm kernel.nas -f elf64 -o kernel.ographic_ui.o: graphic_ui.cpp graphic_ui.hppx86_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 -Wextramain.o: main.cpp graphic_ui.hppx86_64-elf-g++ -c -std=c++17 -m64 -march=x86-64 -fno-builtin -I../libc/include main.cpp -o main.o -Wall -Werror -Wextraentry.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 && popdkernel_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.outkernel_final.bin: kernel_final.outx86_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    rbpmov     rbp, rspsub     rsp, 72mov     DWORD PTR [rbp-180], edi ; amov     QWORD PTR [rbp-168], rsi ; arg1mov     QWORD PTR [rbp-160], rdxmov     QWORD PTR [rbp-152], rcxmov     QWORD PTR [rbp-144], r8mov     QWORD PTR [rbp-136], r9test    al, alje      .L3movaps  XMMWORD PTR [rbp-128], xmm0movaps  XMMWORD PTR [rbp-112], xmm1movaps  XMMWORD PTR [rbp-96], xmm2movaps  XMMWORD PTR [rbp-80], xmm3movaps  XMMWORD PTR [rbp-64], xmm4movaps  XMMWORD PTR [rbp-48], xmm5movaps  XMMWORD PTR [rbp-32], xmm6movaps  XMMWORD PTR [rbp-16], xmm7
.L3:nopleaveret

可以看到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.hqwc.cn/news/224897.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

动态规划专项---状态机模型

文章目录 大盗阿福股票买卖IV股票买卖V设计密码修复DNA 一、大盗阿福OJ链接 本题思路:状态表示当前第i家店铺选择偷或者不偷的最大利益。状态计算:f[i][0]std::max(f[i-1][0],f[i-1][1]);//如果第i家店铺被偷,则第i-1家店铺不能被偷&#xff0c;f[i][1]f[i-1][0]w[i]…

如何实现经济破圈?跟随“绿色积分”先行一步!

近年来&#xff0c;随着消费升级和信息技术的发展&#xff0c;新型消费逐渐成为推动经济增长的重要力量。为了加快发展新型消费&#xff0c;国家政府出台了一系列政策措施&#xff0c;《关于以新业态新模式引流新型消费加快发展》文件就是其中之一。本文将从该文件的背景、目的…

MES系统中的工厂计时计件工资

在制造业中&#xff0c;为了提高工资核算的准确性和效率&#xff0c;实时的数据跟踪和数据处理成为了关键。本文将从多个方面详细介绍MES系统在工厂计时计件工资系统方面的解决方案&#xff0c;以及MES系统与ERP系统如何实现联动集成。 一、MES系统在工厂计时计件工资系统中的解…

4个Python实战项目,让你瞬间读懂Python!

前言 Python 是一种极具可读性和通用性的编程语言。Python 这个名字的灵感来自于英国喜剧团体 Monty Python&#xff0c;它的开发团队有一个重要的基础目标&#xff0c;就是使语言使用起来很有趣。Python 易于设置&#xff0c;并且是用相对直接的风格来编写&#xff0c;对错误…

深入解析Windows操作系统——系统结构

文章目录 需求和设计目标总体结构可移植性对称多处理可伸缩性 关键的系统组件Windows子系统Ntdll.dll执行体内核硬件支持硬件抽象层HAL设备驱动程序 Windows驱动程序模型执行体组件常用的绝大多数函数名前缀 系统进程空闲进程中断和DPCSystem进程和系统线程会话管理器Winlogon、…

NX二次开发UF_CURVE_auto_join_curves 函数介绍

文章作者&#xff1a;里海 来源网站&#xff1a;https://blog.csdn.net/WangPaiFeiXingYuan UF_CURVE_auto_join_curves Defined in: uf_curve.h int UF_CURVE_auto_join_curves(tag_t * crv_list, int crv_num, int join_type, tag_t * join_list, int * join_num ) overvi…

马卡龙产业分析:全球市场规模约19.3亿美元

马卡龙由两个杏仁饼干之间夹有奶油、果酱、巧克力等馅料而组成。它具有光滑的外表和柔软的内在&#xff0c;口感细腻、香甜可口。 精致外观和多彩口味&#xff1a;马卡龙以其精美的外观和丰富多样的口味吸引了消费者。色彩缤纷的外表和各种口味的选择使得马卡龙成为一种视觉和味…

Nginx-进程

Nginx-相关问题_01 Windows关闭所有nginx服务 windows 系统下开发调试时不用每次频繁的 启动->任务管理器->查找进程->结束进程&#xff01; 查看nginx的进程占用情况 tasklist | find /i "nginx.exe" || exit关闭nginx的所有进程 taskkill /im nginx.…

leetcode 1670

leetcode 1670 解题思路 使用2个deque作为类的成员变量 code class FrontMiddleBackQueue { public:deque<int> left;deque<int> right;FrontMiddleBackQueue() {}void pushFront(int val) {left.push_front(val);if(left.size() right.size()2){right.push_fr…

C++之STL库:string类(用法列举和总结)

前言 大家在学习STL库的时候一定要学会看英文文档&#xff0c;俗话说熟能生巧&#xff0c;所以还得多练&#xff01; 在使用string类之前&#xff0c;要包含头文件#include <string>和using namespace std; 文档链接&#xff1a;string - C Reference 一、string——构造…

element table滚动条失效

问题描述:给el-table限制高度之后滚动条没了 给看看咋设置的&#xff1a; <el-table:data"tableData"style"width: 100%;"ref"table"max-height"400"sort-change"changeSort">对比了老半天找不出问题&#xff0c;最后…

DS图—图的最短路径/Dijkstra算法【数据结构】

DS图—图的最短路径/Dijkstra算法【数据结构】 题目描述 给出一个图的邻接矩阵&#xff0c;输入顶点v&#xff0c;用迪杰斯特拉算法求顶点v到其它顶点的最短路径。 输入 第一行输入t&#xff0c;表示有t个测试实例 第二行输入顶点数n和n个顶点信息 第三行起&#xff0c;每行…