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

news2024/11/15 17:41:22

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

加载64位指令

前一节我们已经成功进入了IA-32e模式,但是,却意料之外地体验了一把在IA-32e模式上运行IA-32指令的兼容模式。

前面我们也看到了IA-32e架构下的硬件扩展方式,比如说寄存器都是在原本基础上扩展的,所以,他可以通过只用低32位寄存器的方式,运行IA-32的指令,以此实现高度兼容。

因此,这里的秘密就是在,段配置的一个保留位上,咱们前面讨论过,段描述符的第54和55位是保留位,因为在IA-32模式下不会去解析这两位。但是IA-32e模式下就利用了其中的第55位,用来表示该段是32位模式还是64位模式,当它为1时,CPU将会用64位指令来解析。

但是,前面分好的代码段我们不能动,毕竟内核这里还有一段32位的代码要执行。所以,我们就再分一个64位指令的段。不过64位的段有一个要求,就是「强制平坦」,也就是说,Base配置是无效的,强制按照0x0作为首地址。原因也很简单,因为IA-32e模式强制要求分页,所以他希望操作系统用这种更先进的方式来管理内存,因而分段这里就要求强制平坦。

注意,我们说的强制平坦是仅当第55位置1的情况才会强制平坦,如果这一位是0,那么向下兼容IA-32模式的话,段基址是有效的。

64位段配置如下:

; 5号段-64位代码段
; 基址0x0000,上限0xfffff 
mov [es:0x28], word 0x00ff ; Limit=0x00ff,这是低16位
mov [es:0x2a], word 0x0000 ; Base(无效)16位
mov [es:0x2c], byte 0x0000 ; Base(无效)16~23位
mov [es:0x2d], byte 1_00_1_101_0b ; P=1, DPL=0, S=1, Type=001b, A=0
mov [es:0x2e], byte 1_0_1_0_0000b  ; G=1, D/B=0, L=1(开启64位模式), AVL=0, Limit的高4位是0000
mov [es:0x2f], byte 0x00   ; Base(无效)8

这样,当我们把CS设置成5号段的时候就可以执行64位指令了。为此,咱们在kernel中添加一个64位指令,操作一下R8寄存器,来验证是否能正常执行:

; 进入IA-32e模式
; 刷新cs以进入64位指令模式
jmp 00101_00_0b:ent64 + 0x8000 ; 注意这里,平坦模式下,要从0x0计算偏移量

[bits 64]
ent64:
  mov r8, 0x12345678911

hlt

通过调试指令,可以观察这一句的执行情况:
执行效果

没问题,我们成功在IA-32e模式下运行了64位指令,并且给64位寄存器赋了值。

到此的项目代码将会放在附件(14-1)中,供读者参考。

改造剩余内核代码

既然我们成功进入了64位模式,那么将剩下的代码,改用64位编译模式,就可以链接到当前的内核中,这样我们就可以执行原本编写的C程序了。

C程序的源码是都不用改变的,我们只需要通过调整参数,让编译期按照64位的方式来编译就好了。不过有两个个地方是需要我们来管的,就是asm_func.nas,因为这个文件是用汇编写的,所以我们需要改造成64位指令。另一个地方是进入entry函数之前,有一些段和栈的配置需要改造。接下来我们一个一个来:

asm_func的改造

要改造的点有4处:

  1. 压栈弹栈时要匹配栈的位宽,因此要改成64位寄存器。
  2. 在64位模式下,由于段已经强制平坦了,因此不再允许用es加偏移来操作内存,只能用默认的ds,因此我们要把其中使用es的部分改成ds
  3. 因为64位模式强制平坦,所以,原本的2号段无法使用了,我们得配一个新的数据段与其对齐。对应显存的地址也要改变。
  4. 在32位模式下,C语言规范传参方式都是通过压栈的,因此我们用[rsp + 12]的方法找参数。但是64位模式下,由于通用寄存器数量增加,为了更高效,会优先采用寄存器传参的方式。对于6个寄存器以下的情况,会按照rdi, rsi, rdx, rcx, r8, r9的顺序来传参,当大于6个时才会采用压栈的方式。所以读参方式要改造。

MBR的段配置处加一个6号段:

; 6号段-64位数据段
; 强制平坦模式,基址无效,上限0xffffffff
mov [es:0x30], word 0xffff ; Limit=0xffff,这是低16位
mov [es:0x32], word 0x0000 ; Base无效
mov [es:0x34], byte 0x0000 ; Base无效
mov [es:0x35], byte 1_00_1_001_0b ; P=1, DPL=0, S=1, Type=001b, A=0
mov [es:0x36], byte 1_0_1_0_0000b ; G=1, D/B=0, L=1, AVL=0, Limit的高4位是0000
mov [es:0x37], byte 0x00   ; Base无效

; 下面是gdt信息的配置(暂且放在0x07f00的位置)
mov ax, 0x07f0
mov es, ax
mov [es:0x00], word 55      ; 因为目前配了7个段,长度为56,所以limit为55
mov [es:0x02], dword 0x7e00 ; GDT配置表的首地址
; 把gdt配置进gdtr
lgdt [es:0x00]

asm_func改造后的代码如下:

[bits 64]
section .text 

global SetVMem ; 告诉链接器下面这个标签是外部可用的
SetVMem:
    ; 现场记录
    push rbp
    mov rbp, rsp
    ; 过程中用到的寄存器都要先记录
    push rbx
    push rcx
    push rdx

    ; 64位模式下不允许通过es偏移,所以只能设置ds
    mov bx, ds ; 用bx记录原本的ds,用于后续恢复现场(这里是因为寄存器还够用,如果不够用的话就还是要压栈)
    ; 把es配成数据
    mov dx, 00110_00_0b
    mov ds, dx
    ; 通过参数找到addr和data(64位优先用寄存器传参)
    mov rdx, rdi ; addr
    mov rcx, rsi ; data
    ; 通过偏移地址来操作显存(0xa0000是显存基址)
    mov [rdx+0xa0000], cl  ; 由于data是1字节的,所以其实只有cl是有效数据

    ; 现场还原
    mov ds, bx
    pop rdx
    pop rcx
    pop rbx
    mov rsp, rbp
    pop rbp
    ; 回跳
    ret

kernel的改造

kernel.nas中,进入entry函数之前,我们要做段寄存器的配置,所以我们把dsesss都配置为平坦模式的数据段,也就是6号段,代码如下:

mov ax, 00110_00_0b ; 选择6号段,数据段
mov ss, ax
; ds要跟ss一致
mov ds, ax
; es也初始化为数据段(防止后续出问题,先初始化)
mov es, ax

; 初始化栈
mov rax, 0x1000
mov rsp, rax    ; 设置初始栈顶
mov rbp, rax    ; ebp也记录初始栈顶

extern Entry
call Entry

hlt

配置参数改造

接下来就是通过调整参数,把这些.nas.c通过64位方式编译,并链接起来。

C编译参数要用-m64 -march=x86-64来生成64位的.o文件,nas的编译参数要用-f elf64来生成64位`.o``文件。

链接时也要用-m elf_x86_64参数,而且要注意一个严重问题,由于现在分段是平坦模式了,所以程序加载的内存地址不再是0的偏移量,而是0x8000,所以链接参数要做调整-Ttext=0x8000

最后objcopy时也要制定参数elf64-x86-64,要注意这个指令的参数是用中划线而不是下划线,跟前两个指令要区分开。

完整的kernelmakefile如下:

.PHONY: all
all: kernel_final.bin

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

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 ../libc/libc.a
# 需要用-L指定静态链接库位置
# -lc表示链接libc.a
# 注意kernel.o要放在第一个
	x86_64-elf-ld -m elf_x86_64 -Ttext=0x8000 kernel.o entry.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

同理,调整libc的配置文件如下:

.PHONY: all
all: libc.a

font.o: font.c
	x86_64-elf-gcc -c -m64 -march=x86-64 -fno-builtin font.c -o font.o

stdio.o: stdio.c include/stdio.h
	x86_64-elf-gcc -c -m64 -march=x86-64 -fno-builtin stdio.c -o stdio.o

string.o: string.c include/string.h
	x86_64-elf-gcc -c -m64 -march=x86-64 -fno-builtin string.c -o string.o

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

libc.a: asm_func.o stdio.o string.o font.o
# $^表示所有依赖文件
# ar是制作静态链接库的工具
	x86_64-elf-ar -crv --target=elf64-x86-64 libc.a $^

.PHONY: clean
clean:
	-rm -f *.o libc.a

看一眼64位改造后的成果

都改造完毕后就可以尝试运行了,这是我们第一次在64位模式下运行完整的程序:
运行效果

完美!该部分的项目源码将会放在附件(14-2)中,供读者参考。

加一个C++程序

终于,我们到了邀请最终大咖登场的环节了。既然64位C语言程序已经可以正常运行,那么同理,我们把C++代码编译成elf64格式的文件,链接到Kernel中,照理说就大功告成了。

因此,我们先在工程中建立一个main.cpp,然后在makefile中编写对应的构建命令:

main.o: main.cpp
	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

# 注意链接的时候要加上main.o
kernel_final.out: kernel.o entry.o main.o ../libc/libc.a 
	x86_64-elf-ld -m elf_x86_64 -Ttext=0x8000 kernel.o entry.o main.o -L../libc -lc -o kernel_final.out

这里用来编译C++代码的指令是x86_64-elf-g++,这里我们指定C++17标准,其余参与跟C语言的entry.c相同,不再赘述。

然后我们在main.cpp中实现main函数,但是有一点要注意,因为程序实际的入口是Entry,所以需要在Entry中调用main函数。不过既然已经有了这一步调用,我们索性就把函数返回值打印出来,代码如下:

void Entry() {
  // 背景设置为白色
  SetBackground(0x0f);
  
  extern int main();
  int ret = main();
  printf("main() returned by: %d", ret);
}

接下来我们来实现main函数。有一点需要注意的是,由于C++是支持函数重载的,所以参与链接的函数符号并不仅仅是函数名,还包含了参数信息。这种构建方式是C语言不支持的,因此,我们想在entry.c中调用main.cpp中的main函数,还需要对这个函数进行额外的声明,告诉编译器采用原始C的方式做链接符号。

声明的方法是使用extern "C"关键字。需要知晓的是,用C方式编译的函数不再支持重载,但可以和C语言源码链接上:

extern "C"
int main() {
  return 0;
}

好了,运行一下看看效果吧:
运行效果

大功告成,我们实现了「从裸机启动开始运行一个C++程序」的任务,撒花!!~~

……

真的大功告成了吗?哈哈!当然没那么简单,C++不像C那么纯粹,它存在很多隐含的动作,只是因为目前main函数过于简单,我们还没有踩任何坑而已。

因此,我们不能过于激动,还是要沉下心来继续进行一段修炼。 不过不用操之过急,可以先享受片刻胜利的喜悦,下一章我们再来看看上了C++之后会碰到哪些问题。

到此的项目源码会放入附件(14-3)中,供读者参考。

小结

这一篇我们介绍了如何在IA-32e模式中运行64位指令,还介绍了如何把C语言编译成64位指令,以及配套的asm_func如何改造。最后成功把C++程序加入了项目中。

本篇的所有项目源码将会放在附件(demo_code_14)中,供读者参考。

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

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

相关文章

喜报!美格智能连续五年荣获物联网100强企业

近日,由中国科学院主管、科学出版社主办的《互联网周刊》联合eNet研究院发布了“2023物联网企业100强”榜单,美格智能凭借在技术创新、市场表现、综合实力等多方面的健稳表现连续五年入选榜单。 《互联网周刊》创办于1998年,是最为权威的商业…

15 reids哨兵机制

redis主机默认是10s发送一次心跳给从节点。 从节点默认1s去发送心跳给主节点。 1、原理 当主节点出现故障时,由Redis Sentinel自动完成故障发现和转移,并通知应用方,实现高可用性。 从节点的主要两个作用: 主节点的数据备份。…

Linux——使用kill结束进程并恢复进程

目录 查看进程结束进程修复进程 查看进程 在linux中,关闭某进程之前先查看已经在运行的进程有哪些,使用下面命令查看: ps aux | grep -i apt 命令查看哪个进程正在使用 apt结束进程 结束某线程的命令为: sudo kill -9 PID 命令…

Azure Machine Learning - Azure可视化图像分类操作实战

目录 一、数据准备二、创建自定义视觉资源三、创建新项目四、选择训练图像五、上传和标记图像六、训练分类器七、评估分类器概率阈值 八、管理训练迭代 在本文中,你将了解如何使用Azure可视化页面创建图像分类模型。 生成模型后,可以使用新图像测试该模型…

AT360-6T GNSS 单频高精度授时模块特性参数

AT360-6T 模块具有高灵敏度、低功耗、低cost等优势,可以满足电力授时,通信授时等领域的应用。AT360-6T特点: 1.支持北斗二代/北斗三代信号 2.高精度授时 3.可靠性授时 实时高精度授时 AT360-6T 系列模块的授时秒脉冲抖动可以达到 10ns&am…

2023快速成为接口测试高手:实用指南!

大量线上BUG表明,对接口进行测试可以有效提升产品质量,暴露手工测试时难以发现的问题,同时也能缩短测试周期,提升测试效率。但在实际执行过程中,接口测试被很多同学打上了“上手难,门槛高”的标签。 本文旨…

Vue服务端渲染——同构渲染

Vue.js 可以用于构建客户端应用程序,组件的代码在浏览器中运行,并输出 DOM 元素。同时,Vue.js 还可以在 Node.js 环境中运行,它可以将同样的组件渲染为字符串并发送给浏览器。这实际上描述了 Vue.js 的两种渲染方式,即…

AndroidStudio2022.3.1 Patch3使用国内下载源加速

记录一下这个版本的as在使用国内下载源加速碰到的诸多问题。 一、gradle-8.0-bin.zip下载慢 编辑项目文件夹/gradle/wrapper/gradle-wrapper.properties,文件内容改为如下: #Fri Nov 24 18:50:06 CST 2023 distributionBaseGRADLE_USER_HOME distribu…

汽车电子 -- 根据DBC解析CAN报文

采集的CAN报文,怎么通过DBC解析呢?有一下几种方法。 首先需要确认是CAN2.0 还是CAN FD报文。 还有是 实时解析 和 采集数据 进行解析。 一、CAN2.0报文实时解析: 1、CANTest工具 使用CAN分析仪 CANalyst-II,采集CAN报文。 使用…

嵌入式FPGA IP正在发现更广阔的用武之地

作者:郭道正, Achronix Semiconductor中国区总经理 在日前落幕的“中国集成电路设计业2023年会暨广州集成电路产业创新发展高峰论坛(ICCAD 2023)”上,Achronix的Speedcore™嵌入式FPGA硅知识产权(eFPGA IP&#xff09…

LabVIEW绘制带有多个不同标尺的波形图

LabVIEW绘制带有多个不同标尺的波形图 通过在同一波形图上使用多个轴,可以使用不同的标尺绘制数据。请按照以下步骤操作。 将波形图或图表控件放在前面板上。 1. 右键点击您要创建多个标尺的轴,然后选择复制标尺。例如,如果要为一个…

iview/view-design+vue2实现表单校验

1.iview/view-design介绍 iview是一款基于Vue.js的开源UI组件库,提供了丰富的组件和样式,支持响应式布局和多语言环境。它使用了最新的前端技术,如ES6、Webpack和SASS,让开发者可以快速构建高质量的Web应用程序。 View-design是一…

JavaScript字符串操作指南:跨行表示与模板字面量

背景: 在 JavaScript 中,如果一个字符串需要跨行表示,你可以使用多种方式来实现。 实现: 法一: 使用反斜杠(\)进行换行续行: let str "这是一个跨行的字符串,\ 我在这里使…

400G QSFP-DD DR4光模块最新产品案例应用解析

随着数据中心的快速发展,企业、供应商以及用户对更高、更快速的网络需求日益增长,易天推出的400G QSFP-DD DR4光模块方案可以更好的帮助用户解决这一系列问题,下面跟随小易一起来看看该产品具有哪些方面的特点和优势吧! 一、400G…

kafka精准一次、事务、幂等性

Kafka事务 消息中间件的消息保障的3个级别 At most once 至多一次。数据丢失。At last once 至少一次。数据冗余Exactly one 精准一次。好!!! 如何区分只要盯准提交位移、消费消息这两个动作的时机就可以了。 当:先消费消息、…

【鸿蒙应用ArkTS开发系列】- 云开发入门实战二 实现省市地区三级联动地址选择器组件(上)

目录 概述 云数据库开发 一、创建云数据库的对象类型。 二、预置数据(为对象类型添加数据条目)。 三、部署云数据库 云函数实现业务逻辑 一、创建云函数 二、云函数目录讲解 三、创建resources目录 四、获取云端凭据 五、导出之前创建的元数据…

java.lang.ArrayIndexOutOfBoundsException: (数组越界异常)

java.lang.ArrayIndexOutOfBoundsException: (数组越界异常) 如何解决数组越界异常?1.1条件判断1.2循环结构1.3 try-catch(异常捕获)避免数组越界异常的方法:数组越界异常的调试和排查技巧: 当我…

报错AttributeError: module ‘cv2‘ has no attribute ‘ximgproc‘

报错AttributeError: module ‘cv2’ has no attribute ‘ximgproc’ 首先查看是否安装opencv-contrib-python pip list | grep opencv显示 opencv-contrib-python 4.4.0.46 opencv-python 4.8.1.78 opencv-pyt…

Keil5MDK创建C51工程

Keil5MDK创建C51工程 1.概述 上篇文章介绍了安装Keil5MDK和C51工具,这篇文章介绍工具的使用,首先介绍如何创建一个51单片机工程,写一个demo程序通过编译,烧录到单片机。 第一篇安装工具文章地址:https://blog.csdn.ne…

Maven镜像仓库问题

1.pom文件远程仓库地址 <!--使用aliyun的Maven镜像源提升下载速度--><repositories><repository><id>aliyunmaven</id><name>aliyun</name><url>https://maven.aliyun.com/repository/public</url></repository>&…