泰晓科技 -- 聚焦 Linux - 追本溯源,见微知著!
网站地址:https://tinylab.org

儿童Linux系统,可打字编程学数理化
请稍侯

ELF转二进制(3/4):动态加载和运行

Wu Zhangjin 创作于 2020/03/12

By Falcon of TinyLab.org Dec 09, 2019

背景简介

有一天,某位同学在讨论群聊起来:

除了直接把 C 语言程序编译成 ELF 运行以外,是否可以转成二进制,然后通过第三方程序加载到内存后再运行。

带着这样的问题,我们写了四篇文章,这是其三。

前面两篇分别讨论了如何把一个转成 Binary 的 ELF 作为一个新的 Section 加入到另外一个程序中执行:

如何实现动态加载和运行

本文继续讨论,但是方向是,在运行时加载 Binary 并运行,支持前面两篇中的两种类型的 Binary:绝对数据地址、相对数据地址。

先加载数据位置无关的 Binary 文件

先来考虑最简单的相对数据地址,动态加载后,仅需考虑把加载的 Binary 所在内存范围设置可执行即可。

把文件加载到内存中并设置内存保护属性的最佳方式是 mmap,当然也可以用 malloc/memalgin 分配内存然后用 mprotect 设置内存保护属性,但是需要额外考虑对齐。

可以直接基于 man mmap 中的例子小改一番,先拿到这个原始的例子:

$ man mmap | sed -ne "/Program source/,/SEE ALSO/p" | egrep -v "Program|SEE" | sed -e "s/^       //g" > mmap.orig.c

做完如下修改,得到一个 mmap.new.c:

$ git diff mmap.orig.c mmap.new.c
diff --git a/mmap.orig.c b/mmap.new.c
index 640bcb0..fe039c8 100644
--- a/mmap.orig.c
+++ b/mmap.new.c
@@ -49,11 +49,12 @@ main(int argc, char *argv[])
         length = sb.st_size - offset;
     }

-    addr = mmap(NULL, length + offset - pa_offset, PROT_READ,
+    addr = mmap(NULL, length + offset - pa_offset, PROT_READ|PROT_EXEC,
                 MAP_PRIVATE, fd, pa_offset);
     if (addr == MAP_FAILED)
         handle_error("mmap");

+#if 0
     s = write(STDOUT_FILENO, addr + offset - pa_offset, length);
     if (s != length) {
         if (s == -1)
@@ -62,6 +63,9 @@ main(int argc, char *argv[])
         fprintf(stderr, "partial write");
         exit(EXIT_FAILURE);
     }
+#else
+    ((void (*)(void))addr)();
+#endif

     exit(EXIT_SUCCESS);
 }

上面的改动很简单,一方面是调整内存保护属性为 MAP_EXEC,另外一方面是把映射完的随机地址转换为一个 void (*)void 函数,然后直接执行,也就是调用 Binary,同时把之前的打印到控制台的部分注释掉。

以 -m32 参数编译,确保可以跑 -m32 的代码(继承上面两节):

$ gcc -m32 -o mmap.new mmap.new.c

这个 mmap.new 即可运行第二篇得到的采用相对数据地址的 hello.bin:

$ ./mmap.new ./hello.bin 0
Hello World

再讨论数据位置固定的 Binary 文件

如果要带绝对地址的 hello.bin 呢?

由于数据加载地址是写死的,那意味着必须告知 mmap 映射到一个固定的地址,即 .text 的装载地址,否则数据访问就会出错。mmap 的第一个参数 addr 配合第三个参数 prot(设置为 MAP_FIXED),恰好可以做到。

只是,mmap 要求这个地址必须是对齐到页表的,这个 page size 可以通过 sysconf(_SC_PAGE_SIZE) 拿到。

可是,默认链接的时候,.text 段并不是对齐到 page size 的,对齐到 page size 的是 entry addr,还有一个 0x54 的偏移,即 elf header + program header。这个 0x54 末尾的地址不会是 page size 对齐的。

那意味者链接的时候得“做点手脚”,得强制让 .text 对齐到 page size,我们观察到 0x8046000 这个可以安全使用,因为程序都是从 0x8048000 之后的。当然,这里可用的只有不到 0x2000,8k,比这个大就把这个地址再改小吧。

怎么强制修改 .text 的装载地址呢,一个是上节提到的修改 ld.script,另外一个是直接用 ld 的 -Ttext 参数:

$ as --32 -o hello.o hello.s
$ ld -melf_i386 -o hello hello.o -Ttext=0x8046000

之后,我们再做一些修改,允许传递这个地址给 mmap,得到 mmap.any.c:

$ diff --git a/mmap.orig.c b/mmap.any.c
index 640bcb0..aa23eb2 100644
--- a/mmap.orig.c
+++ b/mmap.any.c
@@ -11,7 +11,7 @@
 int
 main(int argc, char *argv[])
 {
-    char *addr;
+    char *addr = NULL;
     int fd;
     struct stat sb;
     off_t offset, pa_offset;
@@ -19,7 +19,7 @@ main(int argc, char *argv[])
     ssize_t s;

     if (argc < 3 || argc > 4) {
-        fprintf(stderr, "%s file offset [length]\n", argv[0]);
+        fprintf(stderr, "%s file offset [addr]\n", argv[0]);
         exit(EXIT_FAILURE);
     }

@@ -40,20 +40,16 @@ main(int argc, char *argv[])
     }

     if (argc == 4) {
-        length = atoi(argv[3]);
-        if (offset + length > sb.st_size)
-            length = sb.st_size - offset;
-                /* Can't display bytes past end of file */
-
-    } else {    /* No length arg ==> display to end of file */
-        length = sb.st_size - offset;
+        sscanf(argv[3], "%p", &addr);
     }
+    length = sb.st_size - offset;

-    addr = mmap(NULL, length + offset - pa_offset, PROT_READ,
-                MAP_PRIVATE, fd, pa_offset);
+    addr = mmap((void *)addr, length + offset - pa_offset, PROT_READ|PROT_EXEC,
+                MAP_PRIVATE|MAP_FIXED, fd, pa_offset);
     if (addr == MAP_FAILED)
         handle_error("mmap");

+#if 0
     s = write(STDOUT_FILENO, addr + offset - pa_offset, length);
     if (s != length) {
         if (s == -1)
@@ -62,6 +58,9 @@ main(int argc, char *argv[])
         fprintf(stderr, "partial write");
         exit(EXIT_FAILURE);
     }
+#else
+    ((void (*)(void))addr)();
+#endif

     exit(EXIT_SUCCESS);
 }

这个改动把原来的参数 length 换掉,替换为 addr,允许直接通过第三个程序参数设置 .text 的装载地址(确保数据地址有效)。

重新编译 mmap.any.c 并运行:

$ gcc -m32 -o mmap.any mmap.any.c
$ ./mmap.any ./hello.bin 0 0x8046000
Hello World

需要注意的是,这个地址必须与 -Ttext 指定的地址一致。

小结

到这里,ELF转二进制 3 篇文章就完成了,分别讨论了静态嵌入、位置无关和动态加载,接下来还有一篇会讨论,如何动态修改数据地址。

欢迎订阅吴老师的 10 小时 C 语言进阶视频课:《360° 剖析 Linux ELF》,课程提供了超过 70 多份实验材料,其中 15 个例子演示了 15 种程序执行的方法。



Read Related:

Read Latest: