自己动手写一个加载器

news2024/9/29 11:25:54

前言

当在 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 - buf
    syscall                     // );

    movq $60, %rax              // exit (
    movq $0,  %rdi              // status = 0
    syscall                     // );

buf:
    .ascii "hello world\n"

buf_end:

Makefile

minimal: minimal.S
	gcc -S minimal.S > minimal.s
	as minimal.s -o minimal.o
	ld 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.bin

Program 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) n

Program 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 free
	void *start;		 // data addr start, align page size
	void *end;			 // data addr end,   align page size
	void *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 free
	void *start;		 // data addr start, align page size
	void *end;			 // data addr end,   align page size
	void *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 🎈🎈🎈

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

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

相关文章

【Python html常用标签】零基础也能轻松掌握的学习路线与参考资料

学习路线 要深入了解Python html常用标签&#xff0c;需要遵循以下学习路线&#xff1a; 1.1 HTML基础知识&#xff1a;了解HTML语言的起源&#xff0c;HTML文档结构和基本标签。学习HTML标签包含但不限于文本标签&#xff0c;图像标签&#xff0c;链接标签&#xff0c;表格标…

canvas的HTML和JavaScript

文章目录 一、canvas元素二、前期准备1. 坐标系2. canvas属性① 获取canvas元素② 把canvas实例化为2D③ 设置路径颜色④ 设置路径宽度⑤ 设置路径末端形状⑥ 设置路径相连时的相连部分形状⑦ 透明度⑦ 虚线 三、绘制图行1. 绘制线段2. 绘制三角形① 空心三角形② 实心三角形 3…

大学四年,我建议你这么学网络安全

在所有关注我的朋友中&#xff0c;大致分为两类&#xff0c;一类是社会人士&#xff0c;有的是安全老手&#xff0c;有的是其它工作但对安全感兴趣的朋友&#xff0c;另一类应该就是大学生了。 尤其随着国家的号召和知识的普及&#xff0c;越来越多的人开始对网络安全感兴趣&a…

ffmpeg日记1011-过滤器-语法高阶,逻辑,函数使用

Author: wencoo Blog&#xff1a;https://wencoo.blog.csdn.net/ Date: 19/05/2023 Details:文章目录 摘要什么是时间线编辑哪些filter支持时间线编辑时间线编辑中&#xff0c;可以使用哪些预设函数常用预设函数功能即使用方法if(x, y)if(x, y, z)gt(x, y)gte(x, y)lt(x, y)lte…

AHB-to-APB Bridge——04apb_tran、apb_if、apb_drv、mem、apb_mon、apb_agt

apb_if放入所有apb需要的信号&#xff0c;以及cb ifndef APB_IF_SV define APB_IF_SVinterface apb_if;logic pclk;logic prst;logic penable;logic …

C++的stack和queue

stack和queue 1.stackstack的模拟实现 2.queuequeue的模拟实现 3.容器适配器3.1. 什么是容器适配器3.2. STL标准库中stack和queue的底层结构3.3. deque的简单介绍3.3.1. deque原理介绍3.3.2. deque的缺陷3.3.3. 为什么选择deque作为stack和queue的底层默认容器 1.stack stack的…

深度学习-第T7周——咖啡豆识别

深度学习-第T7周——咖啡豆识别 深度学习-第T7周——咖啡豆识别一、前言二、我的环境三、前期工作1、导入数据集2、查看图片数目 四、数据预处理1、 加载数据1、设置图片格式2、划分训练集3、划分验证集4、查看标签 2、数据可视化3、检查数据4、配置数据集 五、搭建CNN网络六、…

Vue3+TS知识点补充

一、关于Ref 1.shallowRef() shallowRef 是 Vue 3 中新引入的响应式数据类型之一&#xff0c;它与 ref 类型非常相似&#xff0c;但是有一些不同点。 不同的是&#xff0c;shallowRef 只会对其包装的对象进行浅层次的响应式处理&#xff0c;即如果这个对象的子属性发生改变&…

软件测试——黑盒测试

1.测试概述 1.1综述 本测试报告为计算机程序能力在线测评系统的黑盒测试&#xff0c;黑盒测试可以在不知道程序内部结构和代码的情况下进行&#xff0c;用来测试软件功能是否符合用户需求&#xff0c;是否达到用户预期目标&#xff0c;是否拥有较好的人机交互体验。 图1.1 黑…

media设备节点初始化与Video4Linux初始化

media设备节点初始化与Video4Linux初始化 文章目录 media设备节点初始化与Video4Linux初始化media设备节点初始化Video4Linux初始化 media设备节点初始化 media_devnode_init函数是一个内核初始化函数&#xff0c;用于在Linux内核启动期间进行设备节点初始化。 函数的主要作用…

复习:遥感图像解译复习整理

惭愧&#xff0c;这个课程从始自终就没有认真学过&#xff0c;一部分是因为自己的原因&#xff0c;另一部分也是因为自己的原因。因此&#xff0c;对于整理的资料有不足之处请指正。 另外&#xff0c;资料自word复制&#xff0c;没有时间整理博客的格式。 -- 2023年05月19日记…

深度学习训练营之Densenet网络

深度学习训练营 原文链接环境介绍前言设计理念网络结构实验结果和讨论pytorch实现DenseNet附录 原文链接 &#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f366; 参考文章&#xff1a;365天深度学习训练营-第J3周&#xff1a;Densenet网络学习&…

第一代AIGC硬件悄然爆发

文 | 智能相对论 作者 | 叶远风 看起来&#xff0c;这可能是一副正常的黑框眼镜&#xff0c;你戴上去彬彬有礼、斯斯文文&#xff1b; 实际上&#xff0c;它里边还装了一个“小伙伴”&#xff0c;你随时可以与它交流&#xff0c;谈天说地或者提出各种问题接受它的帮助&#x…

深度学习之构建MPL神经网络——泰坦尼克号乘客的生存分析

大家好&#xff0c;我是带我去滑雪&#xff01; 本期使用泰坦尼克号数据集&#xff0c;该数据集的响应变量为乘客是生存还是死亡&#xff08;survived&#xff0c;其中1表示生存&#xff0c;0表示死亡&#xff09;&#xff0c;特征变量有乘客舱位等级&#xff08;pclass&#x…

广告投放的关键成功因素:广告归因与广告效果监测

在当今竞争激烈的市场环境中&#xff0c;广告归因和广告效果监测成为了广告投放中至关重要的环节。通过深入了解广告归因和广告效果监测的方法&#xff0c;企业可以更好地评估广告投放的成效&#xff0c;并做出精确的决策&#xff0c;以提高广告效果和最大化投资回报。 本文将带…

昆仑万维“勇闯”百模大战:一个“无懈可击”的商业故事话本?

文丨智能相对论 作者丨沈浪 新能源火了做新能源&#xff0c;元宇宙火了做元宇宙。 如果一个热点领域没有昆仑万维的身影&#xff0c;那一定是这个领域还不够“热”&#xff0c;爆不了。 但凡是热到爆的领域&#xff0c;昆仑万维虽迟但到。 不过&#xff0c;这样说可能有些…

【计算机网络基础】章节测试3 数据链路层

文章目录 判断题选择题辨析题应用题 判断题 相对于广域网而言&#xff0c;局域网的误码率较低。√ 交换机是依据IP地址来转发数据包的。 局域网使用集线器作为网络连接设备时&#xff0c;逻辑上是星型结构。 PPP协议应首先满足的需求是简单&#xff0c;以使得协议在实现的时…

Go语言的学习【2】基础语法

目录 代码组成部分字符串格式化字符数据类型变量遇到的问题及解决办法1 代码组成部分 Go 程序可以由多个标记组成&#xff0c;可以是关键字&#xff0c;标识符&#xff0c;常量&#xff0c;字符串&#xff0c;符号。 在 Go 程序中&#xff0c;一行代表一个语句结束。 如果你…

【linux】图文并茂,让你轻松掌握Linux基本指令

目录 一&#xff0c;前提 二&#xff0c; 在root身份下&#xff0c;管理用户 1. 判断身份 2. 创建用户 3. 销毁用户 三&#xff0c;文件增&#xff0c;删&#xff0c;移动指令 1. pwd——查看路径 2. ps ——打开文件目录 3. touch——创建文件 4. nano——打开文件 5.…

【大数据】Presto(Trino)REST API 与执行计划介绍

文章目录 一、概述二、环境准备三、常用 REST API1&#xff09;worker 节点优雅退出2&#xff09;提交SQL查询请求3&#xff09;获取查询状态4&#xff09;获取查询结果5&#xff09;取消查询请求6&#xff09;获取Presto 节点信息7&#xff09;获取Presto服务器使用统计信息8&…