[置顶] 泰晓 RISC-V 实验箱,配套 30+ 讲嵌入式 Linux 系统开发公开课
[置顶] Linux Lab v1.4 升级部分内核到 v6.10,新增泰晓 RISC-V 实验箱支持,新增最小化内核配置支持大幅提升内核编译速度,在单终端内新增多窗口调试功能等Linux Lab 发布 v1.4 正式版,升级部分内核到 v6.10,新增泰晓实验箱支持
[置顶] 泰晓社区近日发布了一款儿童益智版 Linux 系统盘,集成了数十个教育类与益智游戏类开源软件国内首个儿童 Linux 系统来了,既可打字编程学习数理化,还能下棋研究数独提升智力
ELF转二进制(4/4):动态计算并修改数据加载地址
By Falcon of TinyLab.org Dec 09, 2019
背景简介
有一天,某位同学在讨论群聊起来:
除了直接把 C 语言程序编译成 ELF 运行以外,是否可以转成二进制,然后通过第三方程序加载到内存后再运行。
带着这样的问题,我们写了四篇文章,这是其四。
前面几篇把汇编、静态链接、静态加载、动态加载都讲了,还差一个缺憾,那就是动态链接。
每次得加载到一个特定地址其实不是那么灵活,所以,在不修改代码的前提下,如果能保证加载到任意地址,而且还能访问数据的话,就需要“动态链接”。
动态计算数据地址
就是数据的地址可以动态分配,本文模拟一下这个过程。
这里用到的例子是最原始的 hello.s,未经 hack 的:
# hello.s
#
# as --32 -o hello.o hello.s
# ld -melf_i386 -o hello hello.o
# objcopy -O binary hello hello.bin
#
.text
.global _start
_start:
xorl %eax, %eax
movb $4, %al # eax = 4, sys_write(fd, addr, len)
xorl %ebx, %ebx
incl %ebx # ebx = 1, standard output
movl $.LC0, %ecx # ecx = $.LC0, the addr of string
xorl %edx, %edx
movb $13, %dl # edx = 13, the length of .string
int $0x80
xorl %eax, %eax
movl %eax, %ebx # ebx = 0
incl %eax # eax = 1, sys_exit
int $0x80
.section .rodata
.LC0:
.string "Hello World\xa\x0"
汇编后看看:
$ objdump -dr hello.o
hello.o: file format elf32-i386
Disassembly of section .text:
00000000 <_start>:
0: 31 c0 xor %eax,%eax
2: b0 04 mov $0x4,%al
4: 31 db xor %ebx,%ebx
6: 43 inc %ebx
7: b9 00 00 00 00 mov $0x0,%ecx
8: R_386_32 .rodata
c: 31 d2 xor %edx,%edx
e: b2 0d mov $0xd,%dl
10: cd 80 int $0x80
12: 31 c0 xor %eax,%eax
14: 89 c3 mov %eax,%ebx
16: 40 inc %eax
17: cd 80 int $0x80
我们希望能够动态链接,也就是在运行时根据 Binary 文件加载的位置,动态计算出数据地址(不用修改代码本身)。也就是说确认 R_386_32
这个类型的地址。
这个地址可以用 Binary 文件的加载地址加上 .rodata 相对 Binary 文件开头(即 .text 起始位置)的偏移算出来。
如上所示,.text 的大小是 0x19(0x17+2),那么 .rodata 跟 .text 的偏移刚好是 0x19,那数据地址可以通过 Binary load addr + 0x19 得出。其实这里也可以直接用 readelf -s hello.o 获得(看 Size 部分):
$ readelf -S hello.o | egrep " .text|Size"
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 1] .text PROGBITS 00000000 000034 000019 00 AX 0 0 1
动态修改数据地址
另外一个是,这个地址需要编码回代码里头,即 “8: R_386_32 .rodata” 这里,也就是 .text 中的第 8 个字节开始的一个 32 位地址(the absolute 32-bit address of the symbol into the specified memory location,可参考:c - meaning of R_386_32/R_386_PC32 in .rel.text se…)
上面得到了两个关键数据:
- 数据偏移(相对 .text 起始位置):0x19
- 代码偏移(相对 .text 起始位置):8,这个地方要写入上面的地址
需要做的是,用 .text addr + 0x19 算出数据地址,然后写入 .text addr + 8 这个位置。
说明一下,在实际的动态链接中,上面两个地址都是可以根据 ELF 中的相关数据,动态计算出来的,这里因为是用到了 Binary 文件,我们通过 objdump/readelf
获取到数据访问指令地址和数据偏移后来模拟动态计算。
.text addr 这个地址在运行时确定,这里由 mmap 动态获得,接下来改动代码如下,核心部分如下:
/* dynamic linking: update the data address */
/* offset are gotten from this command: $ objdump -dr hello.o */
#define inst_offset 8
#define data_offset 0x19
*(unsigned int *)(addr + inst_offset) = (uint)addr + data_offset;
addr 即 mmap 加载 Binary 文件到内存时分配的地址,也是 .text 的起始地址(Binary 文件开头就是 .text,接着是 .rodata)。
完整代码如下:
$ diff --git a/mmap.orig.c b/mmap.any.c
index 640bcb0..87358d9 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,25 @@ 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_WRITE|PROT_EXEC,
+ MAP_PRIVATE|MAP_FIXED|MAP_POPULATE, fd, pa_offset);
if (addr == MAP_FAILED)
handle_error("mmap");
+ /* dynamic linking: update the data address */
+ /* offset are gotten from this command: $ objdump -dr hello.o */
+ #define inst_offset 8
+ #define data_offset 0x19
+ *(unsigned int *)(addr + inst_offset) = (uint)addr + data_offset;
+
+ /* flush icache: Using the GNU Compiler Collection (GCC): Other Bui... */
+ __builtin___clear_cache(addr, addr + length + offset - pa_offset);
+
+#if 0
s = write(STDOUT_FILENO, addr + offset - pa_offset, length);
if (s != length) {
if (s == -1)
@@ -62,6 +67,9 @@ main(int argc, char *argv[])
fprintf(stderr, "partial write");
exit(EXIT_FAILURE);
}
+#else
+ ((void (*)(void))addr)();
+#endif
exit(EXIT_SUCCESS);
}
编译和运行如下:
$ gcc -m32 -o mmap.any mmap.any.c
$ sudo ./mmap.any ./hello.bin 0
Hello World
$ ./mmap.any ./hello.bin 0 0x8042000
Hello World
如上演示,这里不再需要提前把 hello 链接到一个“合法”的地址,而是让 mmap 自己合理分配一个,当然,也兼容自己指定一个合法的加载地址。
上述代码还加了一个函数,用于刷新指令 Cache,确保运行结果的一致性,这个函数是 gcc 提供的:__builtin___clear_cache
,详细介绍在 c - Is there a way to flush the entire CPU cache…。
小结
未来可以考虑进一步完善 Binary 文件结构,在后面加入 inst offset 和 data offset 的数组,这样,在运行时就可以借用这个数据,不用在代码中写死。
到这里,四篇文章,比较系统地介绍了如何把一个 ELF 文件转换为 Binary 文件,并通过第三方程序加载和执行,内容涉及静态嵌入、位置无关、动态加载以及动态计算和修改数据地址。
欢迎订阅吴老师的 10 小时 C 语言进阶视频课:《360° 剖析 Linux ELF》,课程提供了超过 70 多份实验材料,其中 15 个例子演示了 15 种程序执行的方法。
猜你喜欢:
- 我要投稿:发表原创技术文章,收获福利、挚友与行业影响力
- 知识星球:独家 Linux 实战经验与技巧,订阅「Linux知识星球」
- 视频频道:泰晓学院,B 站,发布各类 Linux 视频课
- 开源小店:欢迎光临泰晓科技自营店,购物支持泰晓原创
- 技术交流:Linux 用户技术交流微信群,联系微信号:tinylab
支付宝打赏 ¥9.68元 | 微信打赏 ¥9.68元 | |
请作者喝杯咖啡吧 |