自己动手写一个加载器

news/2024/11/25 21:59:37/

前言

当在 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 🎈🎈🎈


http://www.ppmy.cn/news/75043.html

相关文章

Maven 入门教程

Maven是专门为Java项目打造的项目管理和构建工具&#xff0c;类似 NodeJS 的 NPM&#xff0c;PHP 的 composer。 官网&#xff1a;https://maven.apache.org/ 它的主要功能有&#xff1a; 提供了一套标准化的项目结构&#xff1b;提供了一套标准化的构建流程&#xff08;编译…

AcWing 826. 单链表(C++)

目录 1.题目描述 2.AC 1.题目描述 826. 单链表 实现一个单链表&#xff0c;链表初始为空&#xff0c;支持三种操作&#xff1a; 向链表头插入一个数&#xff1b;删除第 kk 个插入的数后面的数&#xff1b;在第 kk 个插入的数后插入一个数。 现在要对该链表进行 MM 次操作…

2、Ray的基本概念和核心组件

2、Ray的基本概念和核心组件 导航 1.简介和背景 2.Ray的基本概念和核心组件 3.分布式任务调度和依赖管理 4.对象存储和数据共享 5.Actor模型和并发编程 6.Ray的高级功能和扩展性 7.使用Ray构建分布式应用程序的案例研究 8.Ray社区和资源 9.核心框架介绍 10.扩展1

04 面向对象(查漏补缺)

this的作用是什么&#xff1f;都有哪些用途&#xff1f; 表示初始化对象当前本身,内部存在地址,只有创建对象的时候才有this 1.调用成员变量,解决成员变量和局部变量的二义性 public Person(String name, int age) {this.name name; //[1] 这是给的创建的对象的字段,不是本…

电脑安装软件时,如何避免捆绑安装?

在网络上非正规网站下载安装软件时&#xff0c;经常会遇到捆绑安装的情况。你明明下载了一个软件&#xff0c;电脑上却多出好几个。那么我们在安装软件时&#xff0c;如何才能避免捆绑安装呢&#xff1f; 什么是捆绑安装&#xff1f; 捆绑安装是指用户安装一个软件时&#xff…

AC规则-2

基于RAM的远程接口 安全元件的访问规则可以通过远程应用程序管理 (RAM) 更新命令进行管理。 因此&#xff0c;ARA-M 和 ARA-C 各自提供一个远程接口&#xff0c;允许在 ARA 中存储或删除访问规则。 访问控制数据的任何远程管理都应仅通过 [GP 卡规范] 定义的安全通道协议来完成…

[VPX611]基于 6U VPX 总线架构的SATA3.0 高性能数据存储板

板卡概述 VPX611 是一款基于6UVPX 总线架构的高性能数据存储板&#xff0c;该板卡采用2 片XilinxKintex-7 系列FPGA 作为主控单元&#xff0c;FPGA 内嵌RAID 控制器&#xff0c;最大支持8 个mSATA 盘&#xff0c;最大存储容量可以达到8TByte&#xff0c;持续数据写入带宽可以达…

人工智能专栏第七讲——自然语言处理

自然语言处理(Natural Language Processing,NLP)是一种研究计算机如何处理和理解人类语言的学科。随着人工智能的发展,自然语言处理已经成为一个越来越重要的领域,涉及到自然语言理解、自然语言生成、语音识别、机器翻译等方面。 自然语言处理的主要挑战是语言的复杂性和…