[置顶] 泰晓 RISC-V 实验箱,配套 30+ 讲嵌入式 Linux 系统开发公开课
ELF转二进制(1/4): 用 objcopy 把 ELF 转成 Binary 并运行
By Falcon of TinyLab.org Dec 09, 2019
背景简介
有一天,某位同学在讨论群聊起来:
除了直接把 C 语言程序编译成 ELF 运行以外,是否可以转成二进制,然后通过第三方程序加载到内存后再运行。
带着这样的问题,我们写了四篇文章,这是其一。
这一篇先介绍如何把 ELF 文件转为二进制文件,然后把二进制文件作为一个 Section 加入到另外一个程序,然后在那个程序中访问该 Section 并运行。
准备工作
先准备一个非常简单的 X86 汇编:
# 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"
这段代码编译、运行后可以打印 Hello World。
通过 objcopy 转换为二进制文件
先来转换为二进制文件,可以用 objcopy。
$ as --32 -o hello.o hello.s
$ ld -melf_i386 -o hello hello.o
$ objcopy -O binary hello hello.bin
分析转换过后的二进制代码和数据
如果要用 objcopy 做成 Binary 还能运行,怎么办呢?
首先来分析一下,转成 Binary 后的代码和数据如下:
$ hexdump -C hello.bin
00000000 31 c0 b0 04 31 db 43 b9 6d 80 04 08 31 d2 b2 0d |1...1.C.m...1...|
00000010 cd 80 31 c0 89 c3 40 cd 80 48 65 6c 6c 6f 20 57 |..1...@..Hello W|
00000020 6f 72 6c 64 0a 00 00
可以发现,刚好只保留了代码和数据部分,其他控制相关的内容全部不见了,非常“纯正”。
再用 objdump 对照看看:
$ objdump -d -j .text hello
hello1: file format elf32-i386
Disassembly of section .text:
08048054 <_start>:
8048054: 31 c0 xor %eax,%eax
8048056: b0 04 mov $0x4,%al
8048058: 31 db xor %ebx,%ebx
804805a: 43 inc %ebx
804805b: b9 6d 80 04 08 mov $0x804806d,%ecx
8048060: 31 d2 xor %edx,%edx
8048062: b2 0d mov $0xd,%dl
8048064: cd 80 int $0x80
8048066: 31 c0 xor %eax,%eax
8048068: 89 c3 mov %eax,%ebx
804806a: 40 inc %eax
804806b: cd 80 int $0x80
需要注意这一行:
804805b: b9 6d 80 04 08 mov $0x804806d,%ecx
在 Binary 中,数据地址是被写死的。
所以,要让 hello.bin 能够运行,必须要把这段 Binary 装载在指定的位置,即:
$ nm hello | grep " _start"
08048054 T _start
这样取到的数据位置才是正确的。
如何运行转换过后的二进制
这个是内核压缩支持的惯用做法,先要取到 Load Address,告诉 wrapper kernel,必须把数据解压到 Load Address 开始的位置。
如果这个要在用户空间做呢?
看上去没那么容易,不过可以这么做:
- 把 hello.bin 作为一个 Section 加入到目标执行代码中,比如叫 run-bin.c
- 然后写 ld script 明确把 hello.bin 放到 Load Address 地址上
- 同时需要修改 ld script 中 run-bin 本身的默认加载地址,否则就覆盖了。也可以先把 hello 的 Load Address 往后搬动,这里用前者。
具体实现
先准备一个 run-bin.c,”bin_entry” 为 hello.bin 的入口,通过 ld script 定义。
#include <stdio.h>
extern void bin_entry(void);
int main(int argc, char *argv[])
{
bin_entry();
return 0;
}
接着,先拿到 run-bin.o:
$ gcc -m32 -c -o run-bin.o run-bin.c
把 hello.bin 作为 .bin
section 加入进 run-bin.o:
$ objcopy --add-section .bin=hello.bin --set-section-flags .bin=contents,alloc,load,readonly run-bin.o
最后,修改链接脚本,先拿到一个默认的链接脚本作为 base:
$ ld -melf_i386 --verbose | sed -ne "/=======/,/=======/p" | grep -v "=======" > ld.script
修改如下,得到一个新的 ld.script.new:
$ git diff ld.script ld.script.new
diff --git a/ld.script b/ld.script.new
index 91f8c5c..7aecbbe 100644
--- a/ld.script
+++ b/ld.script.new
@@ -11,7 +11,7 @@ SEARCH_DIR("=/usr/local/lib/i386-linux-gnu"); SEARCH_DIR("=/lib/i386-linux-gnu")
SECTIONS
{
/* Read-only sections, merged into text segment: */
- PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x08048000)); . = SEGMENT_START("text-segment", 0x08048000) + SIZEOF_HEADERS;
+ PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x08046000)); . = SEGMENT_START("text-segment", 0x08046000) + SIZEOF_HEADERS;
.interp : { *(.interp) }
.note.gnu.build-id : { *(.note.gnu.build-id) }
.hash : { *(.hash) }
@@ -60,6 +60,11 @@ SECTIONS
/* .gnu.warning sections are handled specially by elf32.em. */
*(.gnu.warning)
}
+ .bin 0x08048054:
+ {
+ bin_entry = .;
+ *(.bin)
+ }
.fini :
{
KEEP (*(SORT_NONE(.fini)))
主要做了两笔修改:
- 把 run-bin 的执行地址往前移动到了 0x08046000,避免代码覆盖
获取到 hello 的 _start 入口地址,并把 .bin 链接到这里,可以通过
nm hello | grep " _start"
获取$ nm hello | grep " _start" 08048054 T _start
- 把 bin_entry 指向 .bin section 链接后的入口
最后,用新的链接脚本链接如下:
$ gcc -m32 -o run-bin run-bin.o -T ld.script.new
链接后可以完美运行:
$ ./run-bin
Hello World
小结
在这个基础上,加上压缩/解压支持,就可以类似实现前面文章中提到的 UPX 了,即类似内核压缩/解压支持。
本文的方法不是很灵活,要求必须把 Binary 装载在指定的位置,否则无法正确获取到数据,后面我们继续讨论如何消除这种限制。
另外,本文用到了 ld script 来设定程序和 Section 的加载地址,实际上可以完全通过 objcopy 和 gcc 的参数来指定,从而减少不必要的麻烦。具体用法欢迎订阅吴老师的 10 小时 C 语言进阶视频课:《360° 剖析 Linux ELF》,课程提供了超过 70 多份实验材料,其中 15 个例子演示了 15 种程序执行的方法。
猜你喜欢:
- 我要投稿:发表原创技术文章,收获福利、挚友与行业影响力
- 知识星球:独家 Linux 实战经验与技巧,订阅「Linux知识星球」
- 视频频道:泰晓学院,B 站,发布各类 Linux 视频课
- 开源小店:欢迎光临泰晓科技自营店,购物支持泰晓原创
- 技术交流:Linux 用户技术交流微信群,联系微信号:tinylab
支付宝打赏 ¥9.68元 | 微信打赏 ¥9.68元 | |
请作者喝杯咖啡吧 |