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

前序文章请看:
从裸机启动开始运行一个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, 0x12345678911hlt

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

没问题,我们成功在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 rbpmov rbp, rsp; 过程中用到的寄存器都要先记录push rbxpush rcxpush rdx; 64位模式下不允许通过es偏移,所以只能设置dsmov bx, ds ; 用bx记录原本的ds,用于后续恢复现场(这里是因为寄存器还够用,如果不够用的话就还是要压栈); 把es配成数据mov dx, 00110_00_0bmov ds, dx; 通过参数找到addr和data(64位优先用寄存器传参)mov rdx, rdi ; addrmov rcx, rsi ; data; 通过偏移地址来操作显存(0xa0000是显存基址)mov [rdx+0xa0000], cl  ; 由于data是1字节的,所以其实只有cl是有效数据; 现场还原mov ds, bxpop rdxpop rcxpop rbxmov rsp, rbppop 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 Entryhlt

配置参数改造

接下来就是通过调整参数,把这些.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.binkernel.o: kernel.nasnasm kernel.nas -f elf64 -o kernel.oentry.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 ../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.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

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

.PHONY: all
all: libc.afont.o: font.cx86_64-elf-gcc -c -m64 -march=x86-64 -fno-builtin font.c -o font.ostdio.o: stdio.c include/stdio.hx86_64-elf-gcc -c -m64 -march=x86-64 -fno-builtin stdio.c -o stdio.ostring.o: string.c include/string.hx86_64-elf-gcc -c -m64 -march=x86-64 -fno-builtin string.c -o string.oasm_func.o: asm_func.nasnasm asm_func.nas -f elf64 -o asm_func.olibc.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.cppx86_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.mzph.cn/news/168303.shtml

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

相关文章

牛客 算法题 记负均正II golang实现

题目 HJ105 记负均正II golang 实现 package mainimport ("bufio""fmt""io""os""strconv""strings" )func main() {scanner : bufio.NewScanner(os.Stdin)nums:make([]int,0)sum:0minus:0for scanner.Scan() {l…

quickapp_快应用_DOM是否显示只能通过if指令!

目录 官网概念语法拓展使用三元表达式控制示例 官网 if指令 概念 if条件指令用于控制是否增加或者删除组件。 语法 <组件名 ifbol><组件名>上述语法中只要bol值为ture则显示该组件&#xff0c;若是bol值为false则不显示该组件(并将其从DOM结构中移除)。 拓展…

持续集成交付CICD:GitLabCI 通过trigger触发流水线

目录 一、理论 1.GitLabCI 二、实验 1.搭建共享库项目 2.GitLabCI 通过trigger触发流水线 三、问题 1.项目app02未触发项目app01 2.GitLab 报502网关错误 一、理论 1.GitLabCI (1) 概念 GitLab CI&#xff08;Continuous Integration&#xff09;是一种持续集成工具…

华为云人工智能入门级开发者认证学习笔记

人工智能入门级开发者认证 人工智能定义 定义 人工智能 (Artificial Intelligence) 是研究、延伸和扩展人的智能的理论、方法、技术及应用系统的一门新的技术科学。 强人工智能 vs 弱人工智能 强人工智能&#xff1a;强人工智能观点认为有可能制造出真正能推理&#xff08…

Redis-缓存高可用集群

Redis集群方案比较 哨兵模式 性能和高可用性等各方面表现一般&#xff0c;特别是在主从切换的瞬间存在访问瞬断的情况。另外哨兵模式只有一个主节点对外提供服务&#xff0c;没法支持很高的并发&#xff0c;且单个主节点内存也不宜设置得过大&#xff0c;否则会导致持久化文件过…

机器学习【02】在 Pycharm 里使用 Jupyter Notebook

只有 Pycharm 的 Professional 版才支持 Jupyter Notebook 本教程结束只能在pycharm中使用&#xff0c;下载的库在pycharm选中的虚拟环境中 ssh -L localhost:9999:localhost:8888 usernameip这句话每次都要用 准备 1.服务器安装jupyter sudo snap install jupyter2.在 Jup…

C#学习相关系列之base和this的常用方法

一、base的用法 Base的用法使用场景主要可以概括为两种&#xff1a; 1 、访问基类方法 2、 调用基类构造函数 使用要求&#xff1a;仅允许用于访问基类的构造函数、实例方法或实例属性访问器。从静态方法中使用 base 关键字是错误的。所访问的基类是类声明中指定的基类。 例如&…

怎样通过代理ip提高上网速度

在当今互联网高度发达的时代&#xff0c;我们经常需要使用代理IP来隐藏自己的真实IP地址或提高网络连接速度。然而&#xff0c;有些用户可能会遇到代理IP无法提高网络速度的情况。那么&#xff0c;如何通过代理IP提高上网速度呢&#xff1f;以下是几个技巧&#xff1a; 1.选择…

android 保活的一种有效的方法

android 保活的一种有效的方法 为什么要保活 说起程序的保活,其实很多人都觉得,要在手机上进行保活,确实是想做一些小动作,其实有些正常的场景也是需要我们进行保活的,这样可以增强我们的用户体验。保活就是使得程序常驻内存,这种程序不容易被杀,或者在被杀以后还能完…

【CVE-2021-1675】Spoolsv打印机服务任意DLL加载漏洞分析

漏洞详情 简介 打印机服务提供了添加打印机的接口&#xff0c;该接口缺乏安全性校验&#xff0c;导致攻击者可以伪造打印机信息&#xff0c;在添加新的打印机时实现加载恶意DLL。这造成的后果就是以system权限执行任意代码。 影响版本 windows_10 20h2 windows_10 21h1 win…

第97步 深度学习图像目标检测:RetinaNet建模

基于WIN10的64位系统演示 一、写在前面 本期开始&#xff0c;我们继续学习深度学习图像目标检测系列&#xff0c;RetinaNet模型。 二、RetinaNet简介 RetinaNet 是由 Facebook AI Research (FAIR) 的研究人员在 2017 年提出的一种目标检测模型。它是一种单阶段&#xff08;o…

RabbitMQ 安装(在docker容器中安装)

为什么要用&#xff1f; RabbitMQ是一个开源的消息代理和队列服务器&#xff0c;主要用于在不同的应用程序之间传递消息。它实现了高级消息队列协议&#xff08;AMQP&#xff09;&#xff0c;并提供了一种异步协作机制&#xff0c;以帮助提高系统的性能和扩展性。 RabbitMQ的作…

​LeetCode解法汇总2304. 网格中的最小路径代价

目录链接&#xff1a; 力扣编程题-解法汇总_分享记录-CSDN博客 GitHub同步刷题项目&#xff1a; https://github.com/September26/java-algorithms 原题链接&#xff1a;力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 描述&#xff1a; 给你一个下…

Flink实战(11)-Exactly-Once语义之两阶段提交

0 大纲 [Apache Flink]2017年12月发布的1.4.0版本开始&#xff0c;为流计算引入里程碑特性&#xff1a;TwoPhaseCommitSinkFunction。它提取了两阶段提交协议的通用逻辑&#xff0c;使得通过Flink来构建端到端的Exactly-Once程序成为可能。同时支持&#xff1a; 数据源&#…

【Redis】前言--介绍redis的全局系统观

一.前言 学习是要形成自己的网状知识以及知识架构图&#xff0c;要不最终都还是碎片化的知识&#xff0c;不能达到提升的目的&#xff0c;只有掌握了全貌的知识才是全解&#xff0c;要不只是一知半解。这章会介绍redis的系统架构图&#xff0c;帮助认识redis的设计是什么样的&a…

解决几乎任何机器学习问题 -- 学习笔记(组织机器学习项目)

书籍名&#xff1a;Approaching (Almost) Any Machine Learning Problem-解决几乎任何机器学习问题 此专栏记录学习过程&#xff0c;内容包含对这本书的翻译和理解过程 我们首先来看看文件的结构。对于你正在做的任何项目,都要创建一个新文件夹。在本例中,我 将项目命名为 “p…

笔记:内网渗透流程之信息收集

信息收集 首先&#xff0c;收集目标内网的信息&#xff0c;包括子网结构、域名信息、IP地址范围、开放的端口和服务等。这包括通过主动扫描和渗透测试工具收集信息&#xff0c;以及利用公开的信息源进行信息搜集。 本机信息收集 查看系统配置信息 查看系统详细信息&#xf…

电子桌牌如何赋能数字化会务?以深圳程序员节为例。

10月24日&#xff0c;由深圳市人民政府指导&#xff0c;深圳市工业和信息化局、龙华区人民政府、国家工业信息安全发展研究中心、中国软件行业协会联合主办的2023深圳中国1024程序员节开幕式暨主论坛活动在深圳龙华区启幕。以“领航鹏城发展&#xff0c;码动程序世界”为主题&a…

模拟退火算法应用——求解函数的最小值

仅作自己学习使用 一、问题 需求&#xff1a; 计算函数 的极小值&#xff0c;其中个体x的维数n10&#xff0c;即x(x1,x2,…,x10)&#xff0c;其中每一个分量xi均需在[-20,20]内。因此可以知道&#xff0c;这个函数只有一个极小值点x (0,0,…,0)&#xff0c;且其极小值是0&…

医保线上购药系统:引领医疗新潮流

在科技的驱动下&#xff0c;医疗健康服务正经历一场数字化的革新。医保线上购药系统&#xff0c;不仅是一种医疗服务的新选择&#xff0c;更是技术代码为我们的健康管理带来的全新可能。本文将通过一些简单的技术代码示例&#xff0c;深入解析医保线上购药系统的工作原理和优势…