自己动手写一个加载器

这篇具有很好参考价值的文章主要介绍了自己动手写一个加载器。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

当在 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 🎈🎈🎈文章来源地址https://www.toymoban.com/news/detail-452406.html

到了这里,关于自己动手写一个加载器的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 深度学习——第7章 项目实战:自己动手写一个神经网络模型

    7.1 导入数据集 7.2 定义神经网络输入层、隐藏层、输出层神经元个数 7.3 网络参数W和b初始化 7.4 正向传播过程 7.5 损失函数 7.6 反向传播过程 7.7 网络参数更新 7.8 搭建整个神经网络模型 7.9 模型训练 7.10 模型预测 7.11 隐藏层神经元个数对分类效果的影响 上一课主要介绍了最简

    2024年01月16日
    浏览(43)
  • 自己动手写数据库系统:实现一个小型SQL解释器(中)

    我们接上节内容继续完成SQL解释器的代码解析工作。下面我们实现对update语句的解析,其语法如下: UpdateCmd - INSERT | DELETE | MODIFY | CREATE Create - CreateTable | CreateView | CreateIndex Insert - INSERT INTO ID LEFT_PARAS FieldList RIGHT_PARAS VALUES LEFT_PARS ConstList RIGHT_PARAS FieldList - Field ( COMMA FieldList)?

    2024年02月12日
    浏览(58)
  • 【网络奇缘】- 如何自己动手做一个五类|以太网|RJ45|网络电缆

    ​ ​ 🌈个人主页:  Aileen_0v0 🔥系列专栏:  一见倾心,再见倾城  ---  计算机网络~ 💫个人格言:\\\"没有罗马,那就自己创造罗马~\\\"  本篇文章关于计算机网络的动手小实验---如何自己动手做一个网线, 也是为后面的物理层学习进行铺垫 话不多说,开始今天的学习之旅吧⛵~  目

    2024年02月04日
    浏览(93)
  • 阿里云盘太小啦,所以自己动手写了一个阿里云盘的搜索引擎

    是不是还在为阿里云盘空间太小而烦恼! 好东西太多,奈何就这么点空间,存不下所有东西,何解? 阿里云盘空间不足的问题也让我头疼,虽然有一些免费的阿里云盘的搜索引擎,但那不是我的! 终于决定了,自己编写一个阿里云盘搜索引擎服务! 先看一下成品图: 如果

    2024年02月13日
    浏览(60)
  • 【机器人/小车】自己动手用ESP32手搓一个智能机器人:ESP32-CAM AI Robot(文末附完整工程源码)

    目录 介绍 硬件需求  软件需求 步骤 总结 源码下载 ESP32-CAM是一款集成了Wi-Fi和蓝牙功能的微控制器模块,同时还集成了摄像头接口,使其成为一个非常适合构建智能机器人的选择。在本项目中,我将向您展示如何使用ESP32-CAM模块构建一个

    2024年04月15日
    浏览(64)
  • 【youcans动手学模型】LeNet 模型 MNIST 手写数字识别

    欢迎关注『youcans动手学模型』系列 本专栏内容和资源同步到 GitHub/youcans 本文用 PyTorch 实现 LeNet5 网络模型,使用 MNIST 数据集训练模型,进行手写数字识别。 Yann LeCun (2018年获得图灵奖)等在 1998 年发表论文“Gradient-Based Learning Applied to Document Recognition”,提出的 LeNet5 模型是

    2024年02月06日
    浏览(44)
  • 自己动手写CPU——第一篇

    从本章开始将一步一步实现教学版 openMIPS处理器。首先介绍系统的设计目标,其中详细说明了openMIPS处理器计划实现的5级流水线。 openmips 设计的目标如下: 五级流水线,分别是:取指、译码、执行、访存、回写 哈佛结构。分开的指令和数据接口 32个32位整数寄存器 大端模式

    2023年04月09日
    浏览(45)
  • 自己动手写Docker学习笔记

    本文为《自己动手写 Docker》的学习,对于各位学习 docker 的同学非常友好,非常建议买一本来学习。 书中有摘录书中的一些知识点,不过限于篇幅,没有全部摘录 (主要也是懒) 。项目仓库地址为:JaydenChang/simple-docker (github.com) 1.1 kernel kernel (内核) 指大多数操作系统的核心部

    2024年02月05日
    浏览(45)
  • 自己动手绕线圈电感详细计算公式

    加载其电感量按下式计算:线圈公式 阻抗(ohm)=2 3.14159 F(工作频率) 电感量(mH),设定需用360ohm阻抗,因此:电感量(mH)=阻抗(ohm)÷(2 3.14159)÷F(工作频率)=360÷(2 3.14159)÷7.06=8.116mH 据此可以算出绕线圈数: 圈数=[电感量 {(18 圈直径(吋))+(40 圈长(吋))}]÷圈直径(吋) 圈数=[8.116*{(18 2.047)+

    2024年02月17日
    浏览(29)
  • python模块: pygame(自己动手写游戏)

    目录 一、pygame的安装  二、pygame基础操作 1.基本窗体设置 2.surface组件  3.event事件 一、pygame的安装 在pycharm 左下角的终端上输入指令pip install pygame,按下回车键执行下载,推荐下载到虚拟环境上,即路径前方带有(venv)。或者不使用pycharm,在控制窗口输入执行该命令也可以(控

    2024年03月21日
    浏览(51)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包