当前位置: 首页 > news >正文

自己动手写一个加载器

前言

当在 linux 命令行中 ./ 运行一个程序时,实际上操作系统会调用加载器将这个程序加载到内存中去执行。为了探究加载器的行为,今天我们就自己动手写一个简单的加载器。

工作原理

加载器的工作原理:

  1. 从磁盘读取 bin 文件到内存,(bin 文件包含的是 CPU 可以直接执行的指令)
  2. 跳转到该内存的起始地址

就这么简单。
理论是比较简单的,但工程实践上可能会遇到各种各样的问题,我们只要围绕主线,遇神杀神,遇魔杀魔,就可以了。千万不要花过多精力去打副本(如果你精力很旺盛,当我没说)。
上面讲,会遇到各种各样的问题,这里不是为了劝退,而是想让大家跟着我一起披荆斩棘,抵达终点,享受整个过程。

bin 程序

在写加载器之前,我们先写一个 bin 程序,不然我们徒有加载器也无法验证其是否能够工作。
bin 程序的功能也很简单,就是向标准输出打印一行字符串。
由于我们计划写的加载器功能比较简单,所以我们写的 bin 程序也要尽可能简单,不要有依赖的动态库。
minimal.S

.global _start
_start:movq $1, %rax               // write (movq $1, %rdi               // fd = 1,lea buf(%rip), %rsi         // buf,movq $(buf_end - buf), %rdx // count = buf_end - bufsyscall                     // );movq $60, %rax              // exit (movq $0,  %rdi              // status = 0syscall                     // );buf:.ascii "hello world\n"buf_end:

Makefile

minimal: minimal.Sgcc -S minimal.S > minimal.sas minimal.s -o minimal.old minimal.o -o $@objcopy -O binary --only-section=.text minimal minimal.bin

上面的代码用 C 语言写出来就是下面两行

sys_write(1, buf, count);
sys_exit(0);

解释下上面的汇编代码:
系统调用号通过 rax 传递,其余参数传递顺序为:rdi,rsi,rdx,r10,r8,r9。

加载器代码

根据上面介绍的工作原理,下面开始写加载器
loader.c

#include <stdio.h>
#include <stdlib.h>int main(int argc, char *argv[])
{FILE *f;char *buffer;long file_size;if (argc < 2) {printf("Usage: %s <filename>\n", argv[0]);return 1;}// 打开二进制文件f = fopen(argv[1], "rb");if (!f) {printf("Error: could not open file %s\n", argv[1]);return 1;}// 获取文件大小fseek(f, 0, SEEK_END);file_size = ftell(f);fseek(f, 0, SEEK_SET);// 分配内存并读取文件内容buffer = (char *)malloc(file_size);if (!buffer) {printf("Error: could not allocate memory\n");fclose(f);return 1;}fread(buffer, file_size, 1, f);// 关闭文件fclose(f);// 转移到二进制文件的入口点void (*entry_point)() = (void (*)())buffer;entry_point();// 释放内存free(buffer);return 0;
}

编译,运行

$ gcc -g -o loader loader.c
$ ./loader ../loader/minimal.bin 
段错误 (核心已转储)

在这里插入图片描述出现了段错误

定位错误

使用 gdb 定位出错位置

Reading symbols from ./loader...
(gdb) set args ../loader/minimal.bin
(gdb) run
Starting program: /home/liyongjun/project/c/C_study/others/loader2/loader ../loader/minimal.binProgram received signal SIGSEGV, Segmentation fault.
0x000055555555a490 in ?? ()

loader 是使用 -g 选项编译出来的,如果出错位置在 loader 中,gdb 会定位到出错的代码行,上面显然没有,那只有一个原因,loader 已经跳转到 minimal.bin 开始执行了,出错位置在 minimal.bin 中。
继续使用 gdb 单步调试一下

34		fread(buffer, file_size, 1, f);
(gdb) 
37		fclose(f);
(gdb) 
40		void (*entry_point)() = (void (*)())buffer;
(gdb) 
41		entry_point();
(gdb) p/x buffer
$1 = 0x55555555a490
(gdb) nProgram received signal SIGSEGV, Segmentation fault.
0x000055555555a490 in ?? ()
(gdb) 

确实出错位置在 bin 程序的入口。
并且出错时收到了信号 SIGSEGV,一般收到该信号是代码访问了空指针、内存越界等。显然我们不属于以上情况。
经过查阅资料得知,如果想执行某处内存的代码,那么该内存需要具有可执行权限
所以收到 SIGSEGV 信号原来是执行了不具有可执行权限的内存代码。

给内存加权限

知道原因就好办了,那就给内存加上可执行权限呗。

在 Linux 中,mprotect() 函数可以用来修改一段指定内存区域的保护属性。

参考:Linux中mprotect()函数的用法

完善代码

	// 将内存页的保护属性修改为可读、可写、可执行if (mprotect(buffer, file_size, PROT_READ | PROT_WRITE | PROT_EXEC) != 0) {perror("Failed to set memory protection");free(buffer);return 1;}

执行,有报错了

$ ./loader ../loader/minimal.bin 
Failed to set memory protection: Invalid argument

无法给 buffer 赋予可执行权限。
查阅资料得知:

mprotect 的参数分别为:内存区间的地址,区间的大小,新的保护标志设置。所指定的内存区间必须包含整个页:区间地址必须和整个系统页大小对齐,而区间长度必须是页大小的整数倍

继续改进代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>struct mem_align {void *origin_start;	 // for freevoid *start;		 // data addr start, align page sizevoid *end;			 // data addr end,   align page sizevoid *origin_end;
};int malloc_align_page(size_t memsize, struct mem_align *mem)
{if (memsize == 0 || mem == NULL)return -1;memset(mem, 0, sizeof(*mem));long pagesize = sysconf(_SC_PAGE_SIZE);if (pagesize == -1) {perror("sysconf err");return -1;}size_t datasize = memsize + pagesize * 2;mem->origin_start = malloc(datasize);if (mem->origin_start == NULL)return -1;mem->origin_end = mem->origin_start + datasize;long mask = pagesize - 1;mem->start = (void *)((long)(mem->origin_start + pagesize) & ~mask);long pagenum = memsize / pagesize + 1;mem->end = mem->start + pagesize * pagenum;return 0;
}int main(int argc, char *argv[])
{FILE *f;char *buffer;long file_size;struct mem_align mem;int ret;if (argc < 2) {printf("Usage: %s <filename>\n", argv[0]);return 1;}// 打开二进制文件f = fopen(argv[1], "rb");if (!f) {printf("Error: could not open file %s\n", argv[1]);return 1;}// 获取文件大小fseek(f, 0, SEEK_END);file_size = ftell(f);fseek(f, 0, SEEK_SET);ret = malloc_align_page(file_size, &mem);if (ret != 0) {printf("malloc error\n");fclose(f);return 1;}fread(mem.start, file_size, 1, f);// 关闭文件fclose(f);// 将内存页的保护属性修改为可读、可写、可执行if (mprotect(mem.start, file_size, PROT_READ | PROT_WRITE | PROT_EXEC) != 0) {perror("Failed to set memory protection");free(mem.origin_start);return 1;}// 转移到二进制文件的入口点void (*entry_point)() = (void (*)())mem.start;entry_point();// 释放内存free(mem.origin_start);return 0;
}

执行

$ ./loader ../loader/minimal.bin 
hello world

成功加载了 minimal.bin,并执行成功。✌✌✌

权限探索

在 loader.c 加些打印,并让程序暂停,我们去查看下内存情况
loader.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>struct mem_align {void *origin_start;	 // for freevoid *start;		 // data addr start, align page sizevoid *end;			 // data addr end,   align page sizevoid *origin_end;
};int malloc_align_page(size_t memsize, struct mem_align *mem)
{if (memsize == 0 || mem == NULL)return -1;memset(mem, 0, sizeof(*mem));long pagesize = sysconf(_SC_PAGE_SIZE);if (pagesize == -1) {perror("sysconf err");return -1;}printf("pagesize : 0x%lx\n", pagesize);size_t datasize = memsize + pagesize * 2;mem->origin_start = malloc(datasize);if (mem->origin_start == NULL)return -1;mem->origin_end = mem->origin_start + datasize;long mask = pagesize - 1;mem->start = (void *)((long)(mem->origin_start + pagesize) & ~mask);long pagenum = memsize / pagesize + 1;mem->end = mem->start + pagesize * pagenum;return 0;
}int main(int argc, char *argv[])
{FILE *f;char *buffer;long file_size;struct mem_align mem;int ret;if (argc < 2) {printf("Usage: %s <filename>\n", argv[0]);return 1;}// 打开二进制文件f = fopen(argv[1], "rb");if (!f) {printf("Error: could not open file %s\n", argv[1]);return 1;}// 获取文件大小fseek(f, 0, SEEK_END);file_size = ftell(f);fseek(f, 0, SEEK_SET);ret = malloc_align_page(file_size, &mem);if (ret != 0) {printf("malloc error\n");fclose(f);return 1;}fread(mem.start, file_size, 1, f);printf("mem start           : %p\n", mem.start);printf("mem end             : %p\n", mem.end);printf("mem origin_start    : %p\n", mem.origin_start);printf("mem origin_end      : %p\n", mem.origin_end);// 关闭文件fclose(f);// 将内存页的保护属性修改为可读、可写、可执行if (mprotect(mem.start, file_size, PROT_READ | PROT_WRITE | PROT_EXEC) != 0) {perror("Failed to set memory protection");free(mem.origin_start);return 1;}sleep(600);// 转移到二进制文件的入口点void (*entry_point)() = (void (*)())mem.start;entry_point();// 释放内存free(mem.origin_start);return 0;
}

运行

$ ./loader ../loader/minimal.bin 
pagesize : 0x1000
mem start           : 0x55fd6152f000
mem end             : 0x55fd61530000
mem origin_start    : 0x55fd6152e8a0
mem origin_end      : 0x55fd615308da

$ ps -ef | grep loader
liyongj+ 1656575 1625198 0 12:45 pts/121 00:00:00 ./loader …/loader/minimal.bin
liyongjun@Box:/proc/1656575$ cat /proc/1656575/maps
55fd5fe51000-55fd5fe52000 r–p 00000000 08:05 3244524 /home/liyongjun/project/c/C_study/others/loader2/loader
55fd5fe52000-55fd5fe53000 r-xp 00001000 08:05 3244524 /home/liyongjun/project/c/C_study/others/loader2/loader
55fd5fe53000-55fd5fe54000 r–p 00002000 08:05 3244524 /home/liyongjun/project/c/C_study/others/loader2/loader
55fd5fe54000-55fd5fe55000 r–p 00002000 08:05 3244524 /home/liyongjun/project/c/C_study/others/loader2/loader
55fd5fe55000-55fd5fe56000 rw-p 00003000 08:05 3244524 /home/liyongjun/project/c/C_study/others/loader2/loader
55fd6152d000-55fd6152f000 rw-p 00000000 00:00 0 [heap]
55fd6152f000-55fd61530000 rwxp 00000000 00:00 0 [heap]
55fd61530000-55fd6154e000 rw-p 00000000 00:00 0 [heap]
7f3c24b32000-7f3c24b54000 r–p 00000000 08:05 658174 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f3c24b54000-7f3c24ccc000 r-xp 00022000 08:05 658174 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f3c24ccc000-7f3c24d1a000 r–p 0019a000 08:05 658174 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f3c24d1a000-7f3c24d1e000 r–p 001e7000 08:05 658174 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f3c24d1e000-7f3c24d20000 rw-p 001eb000 08:05 658174 /usr/lib/x86_64-linux-gnu/libc-2.31.so
7f3c24d20000-7f3c24d26000 rw-p 00000000 00:00 0
7f3c24d40000-7f3c24d41000 r–p 00000000 08:05 658162 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f3c24d41000-7f3c24d64000 r-xp 00001000 08:05 658162 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f3c24d64000-7f3c24d6c000 r–p 00024000 08:05 658162 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f3c24d6d000-7f3c24d6e000 r–p 0002c000 08:05 658162 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f3c24d6e000-7f3c24d6f000 rw-p 0002d000 08:05 658162 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7f3c24d6f000-7f3c24d70000 rw-p 00000000 00:00 0
7ffec3fac000-7ffec3fce000 rw-p 00000000 00:00 0 [stack]
7ffec3fe1000-7ffec3fe5000 r–p 00000000 00:00 0 [vvar]
7ffec3fe5000-7ffec3fe7000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]

0x55fd6152f000 ~ 0x55fd61530000,是我们使用 malloc 从堆 (heap) 申请的内存,已经被我们赋予了可执行 (x) 权限。

over 🎈🎈🎈

相关文章:

自己动手写一个加载器

前言 当在 linux 命令行中 ./ 运行一个程序时&#xff0c;实际上操作系统会调用加载器将这个程序加载到内存中去执行。为了探究加载器的行为&#xff0c;今天我们就自己动手写一个简单的加载器。 工作原理 加载器的工作原理&#xff1a; 从磁盘读取 bin 文件到内存&#xf…...

C# 性能优化和Unity性能优化

C# 性能优化 C# 性能优化是一个非常广泛的话题&#xff0c;需要从各个方面来考虑&#xff0c;包括算法和数据结构、编译器优化、代码优化等等。下面是一些常见的 C# 性能优化技巧&#xff1a; 选择正确的数据结构&#xff1a;C# 提供了各种不同的数据结构&#xff0c;例如数组、…...

面试题背麻了,花3个月面过华为测开岗,拿个26K不过分吧?

计算机专业&#xff0c;代码能力一般&#xff0c;之前有过两段实习以及一个学校项目经历。第一份实习是大二暑期在深圳的一家互联网公司做前端开发&#xff0c;第二份实习由于大三暑假回国的时间比较短&#xff08;小于两个月&#xff09;&#xff0c;于是找的实习是在一家初创…...

跟着我学 AI丨教育 + AI = 一对一教学

随着人工智能&#xff08;AI&#xff09;技术的迅速发展&#xff0c;它已经开始了改变教育的方式。本文将介绍AI在教育行业中的应用场景&#xff0c;当前从事AI 教育的公司有哪些以及这些公司所提供的教育产品的特点&#xff0c;和未来AI 教育的潜在实现方式。 AI在教育行业的…...

1-动态规划算法理论基础

目录 1.什么是动态规划? PS&#xff1a;动态规划 VS 贪心 2.动态规划的解题步骤 ①确定dp数组&#xff08;dp table&#xff09;以及下标的含义。 ②确定递推公式/状态转移公式。 ③dp数组如何初始化。 ④确定遍历顺序。 ⑤举例推导dp数组。 3.动态规划应该如何debug…...

kafka延时队列内部应用简介

kafka延时队列_悠然予夏的博客-CSDN博客 两个follower副本都已经拉取到了leader副本的最新位置&#xff0c;此时又向leader副本发送拉取请求&#xff0c;而leader副本并没有新的消息写入&#xff0c;那么此时leader副本该如何处理呢&#xff1f;可以直接返回空的拉取结…...

【网络】HTTPHTTPS协议

文章目录 HTTP协议认识URLurlencode和urldecodeHTTP协议格式HTTP请求协议格式简单的小实验 HTTP响应协议格式关于封装解包分用 HTTP的方法关于GET和POST方法概念GET&POST对比(代码测试)测试POST和GET方法的区别 HTTP的状态码关于重定向的状态码临时重定向的代码演示: HTTP的…...

因子图优化

最大后验概率估计问题 我们常将状态估计问题建模为最大后验估计&#xff08;MAP&#xff09;。之所以命名为最大后验估计&#xff0c;是因为它在给定了观测 Z \bm Z Z的情况下&#xff0c;最大化状态 X \bm X X的后验概率密度 p ( X ∣ Z ) p(\bm X|\bm Z) p(X∣Z) X M A P …...

JVM 垃圾回收相关算法

垃圾标记阶段 对象存活判断: 在堆里存放着几乎所有的 Java 对象实例&#xff0c;在GC 执行垃圾回收之前&#xff0c;首先需要区分出内存中哪些是存活对象&#xff0c;哪些是已经死亡的对象。只有被标记为已经死亡的对象&#xff0c;GC 才会在执行垃圾回收时&#xff0c;释放掉…...

[Bing Chat] 以某某这个数据结构 编一个故事 要求不能说出某某这个数据结构

以二叉搜索树这个数据结构 编一个故事 要求不能说出二叉搜索树这个数据结构 好的&#xff0c;我试试。这是我编的一个故事&#xff0c;希望你喜欢&#xff1a; 有一天&#xff0c;小明要去参加一个数学竞赛&#xff0c;他需要在有限的时间内回答很多问题。他发现&#xff0c;有…...

【算法】【算法杂谈】一种字符串和数字的对应关系

目录 前言问题介绍解决方案代码编写java语言版本c语言版本c语言版本 思考感悟写在最后 前言 当前所有算法都使用测试用例运行过&#xff0c;但是不保证100%的测试用例&#xff0c;如果存在问题务必联系批评指正~ 在此感谢左大神让我对算法有了新的感悟认识&#xff01; 问题介…...

Java并发基础理论

Java并发基础理论 进程与线程 进程 ​ 进程是程序的一次执行过程&#xff0c;是系统运行程序的基本单位&#xff0c;因为进程是动态的。系统运行一个程序就是一个进程从创建运行到消亡的过程。 ​ 我们启动main方法其实就是启动了一个JVM进程&#xff0c;而main方法所在的线…...

ubuntu22.04静态ip设置(桥接模式、only-host+NAT模式)

在创建一台虚拟机后&#xff0c;默认的方式往往是通过DHCP动态的进行分配&#xff0c;DHCP服务器会告知创建的虚拟机分配到的ip地址&#xff0c;网关地址等信息。所以在创建好虚拟机之后&#xff0c;这些信息都不需要我们来配置&#xff0c;我们直接用就好了。 但是&#xff0…...

深度模型中的正则化、梯度裁剪、偏置初始化操作

最近调试代码&#xff0c;发现怎么调试都不行&#xff0c;就想着用一些优化方式&#xff0c;然后又不是很清楚这些优化方式的具体细节&#xff0c;然后就学习了一下&#xff0c;这里记录下来&#xff0c;方便以后查阅。 深度模型中的正则化、梯度裁剪、偏置初始化操作 正则化常…...

设计模式之装饰模式

定义 装饰模式指的是在不必改变原类文件和使用继承的情况下&#xff0c;动态地扩展一个对象的功能。它是通过创建一个包装对象&#xff0c;也就是装饰来包裹真实的对象。 模式特点 &#xff08;1&#xff09; 装饰对象和真实对象有相同的接口。这样客户端对象就能以和真实对…...

华为OD机试真题 Java 实现【最佳对手】【2023Q1 200分】

一、题目描述 游戏里面,队伍通过匹配实力相近的对手进行对战。但是如果匹配的队伍实力相差太大,对于双方游戏体验都不会太好。 给定 n 个队伍的实力值,对其进行两两实力匹配,两支队伍实例差距在允许的最大差距 d内,则可以匹配。 要求在匹配队伍最多的情况下匹配出的各组…...

IOS证书制作教程

IOS证书制作教程 点击苹果证书 按钮 点击新增 输入证书密码&#xff0c;名称 这个密码不是账号密码&#xff0c;而是一个保护证书的密码&#xff0c;是p12文件的密码&#xff0c;此密码设置后没有其他地方可以找到&#xff0c;忘记了只能删除证书重新制作&#xff0c;所以请务…...

【人工智能】蚁群算法(密恐勿入)

蚁群算法&#xff08;密恐勿入&#xff09; 蚁群算法--给你一个感性认识 蚁群算法&#xff08;密恐勿入&#xff09;1. 算法简介1.1 基本原理1.1.1 模拟蚂蚁在简单地形&#xff0c;寻找食物1.1.2 模拟蚂蚁在复杂地形&#xff0c;找到食物1.2 算法应用 2. 算法解析3.算法应用——…...

VONR排查指导分享

不能注册或呼叫到SIP服务器端30秒挂断呼叫的黄金法则咬线或摘机状态单通或无语音收到400 bad request收到413&#xff0c;513 Request Entity Too Large或Message Too Large消息收到408&#xff0c; 480或者487 消息483 - Too Many Hops488 – Not Acceptable Here语音质量和思…...

Daftart.ai:人工智能专辑封面生成器

前言 Daft Art AI是一款使用人工智能技术来帮助您制作专辑封面的软件&#xff0c;它可以让您在几分钟内&#xff0c;用简单的编辑器和精选的美学风格&#xff0c;为您的专辑或歌曲创建出惊艳的高质量的艺术品。Daft Art AI有以下几个特点&#xff1a;简单易用&#xff1a;您只…...

TDengine 快速体验(Docker 镜像方式)

简介 TDengine 可以通过安装包、Docker 镜像 及云服务快速体验 TDengine 的功能&#xff0c;本节首先介绍如何通过 Docker 快速体验 TDengine&#xff0c;然后介绍如何在 Docker 环境下体验 TDengine 的写入和查询功能。如果你不熟悉 Docker&#xff0c;请使用 安装包的方式快…...

【位运算】消失的两个数字(hard)

消失的两个数字&#xff08;hard&#xff09; 题⽬描述&#xff1a;解法&#xff08;位运算&#xff09;&#xff1a;Java 算法代码&#xff1a;更简便代码 题⽬链接&#xff1a;⾯试题 17.19. 消失的两个数字 题⽬描述&#xff1a; 给定⼀个数组&#xff0c;包含从 1 到 N 所有…...

如何在看板中有效管理突发紧急任务

在看板中有效管理突发紧急任务需要&#xff1a;设立专门的紧急任务通道、重新调整任务优先级、保持适度的WIP&#xff08;Work-in-Progress&#xff09;弹性、优化任务处理流程、提高团队应对突发情况的敏捷性。其中&#xff0c;设立专门的紧急任务通道尤为重要&#xff0c;这能…...

1.3 VSCode安装与环境配置

进入网址Visual Studio Code - Code Editing. Redefined下载.deb文件&#xff0c;然后打开终端&#xff0c;进入下载文件夹&#xff0c;键入命令 sudo dpkg -i code_1.100.3-1748872405_amd64.deb 在终端键入命令code即启动vscode 需要安装插件列表 1.Chinese简化 2.ros …...

.Net Framework 4/C# 关键字(非常用,持续更新...)

一、is 关键字 is 关键字用于检查对象是否于给定类型兼容,如果兼容将返回 true,如果不兼容则返回 false,在进行类型转换前,可以先使用 is 关键字判断对象是否与指定类型兼容,如果兼容才进行转换,这样的转换是安全的。 例如有:首先创建一个字符串对象,然后将字符串对象隐…...

均衡后的SNRSINR

本文主要摘自参考文献中的前两篇&#xff0c;相关文献中经常会出现MIMO检测后的SINR不过一直没有找到相关数学推到过程&#xff0c;其中文献[1]中给出了相关原理在此仅做记录。 1. 系统模型 复信道模型 n t n_t nt​ 根发送天线&#xff0c; n r n_r nr​ 根接收天线的 MIMO 系…...

C++使用 new 来创建动态数组

问题&#xff1a; 不能使用变量定义数组大小 原因&#xff1a; 这是因为数组在内存中是连续存储的&#xff0c;编译器需要在编译阶段就确定数组的大小&#xff0c;以便正确地分配内存空间。如果允许使用变量来定义数组的大小&#xff0c;那么编译器就无法在编译时确定数组的大…...

SiFli 52把Imagie图片,Font字体资源放在指定位置,编译成指定img.bin和font.bin的问题

分区配置 (ptab.json) img 属性介绍&#xff1a; img 属性指定分区存放的 image 名称&#xff0c;指定的 image 名称必须是当前工程生成的 binary 。 如果 binary 有多个文件&#xff0c;则以 proj_name:binary_name 格式指定文件名&#xff0c; proj_name 为工程 名&…...

JVM 内存结构 详解

内存结构 运行时数据区&#xff1a; Java虚拟机在运行Java程序过程中管理的内存区域。 程序计数器&#xff1a; ​ 线程私有&#xff0c;程序控制流的指示器&#xff0c;分支、循环、跳转、异常处理、线程恢复等基础功能都依赖这个计数器完成。 ​ 每个线程都有一个程序计数…...

QT3D学习笔记——圆台、圆锥

类名作用Qt3DWindow3D渲染窗口容器QEntity场景中的实体&#xff08;对象或容器&#xff09;QCamera控制观察视角QPointLight点光源QConeMesh圆锥几何网格QTransform控制实体的位置/旋转/缩放QPhongMaterialPhong光照材质&#xff08;定义颜色、反光等&#xff09;QFirstPersonC…...