本 LoongArch 指令集研究工作在百度贴吧龙芯吧同步连载。
本研究中涉及的逆向工程仅出于学习、研究目的。本研究工作未得到任何龙芯、麒麟等软硬件厂商的任何形式帮助。
本研究属于个人行为,与本人雇主或任何其他主体无关。
上一回我们讲到,解包出来的二进制真的是 LoongArch 的 ELF 文件,接下来我们可以有两个方向:一是释读 LoongArch ELF 格式的特殊内容;二是直接动手,搞明白指令含义。
ELF 文件格式支持每个架构各自表达一些特定属性、tag、section。这么做也是合情合理,正如没有两片雪花完全相同,不同架构当然也有各自独特的属性。你不会看到 AMD64 的程序库声明自己不支持浮点指令,也不会看到 ARM 可执行文件包含 MIPS 格式的重定向记录。由于 ELF 格式本身就考虑了这部分需求,我们不需要先行了解任何 LoongArch 的信息,就可以搞明白很多东西——
然而,实际操作中当然需要先弄懂指令集,才能真正把这些架构相关扩展整明白。例如你看到一个类型 3 的重定向,地址是 78f00
,内容是 1a2b3c
,你并不能直接知道它的含义是把“什么东西”写到“什么位置”。到底是把 1a2b3c
左移 10 位然后合并到 78f00 - 代码段基址
的指令字里?还是用 1a2b3c
先减去 78f00
构造一个 PC-relative 偏移量然后左移 6 位写入相同的地方?
那我们还是先想办法把指令都提取出来再说吧!
有一个工具叫 readelf
,顾名思义,它 reads ELF files
;你可以自己不带参数,或者带着 --help
运行一下,支持的功能挺多的。我们直接让他解析文件头:
$ readelf -eW libpython2.7_d.so.1.0
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: <unknown>: 0x102
Version: 0x1
Entry point address: 0x52a10
Start of program headers: 64 (bytes into file)
Start of section headers: 9896856 (bytes into file)
Flags: 0x3
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 6
Size of section headers: 64 (bytes)
Number of section headers: 34
Section header string table index: 33
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .note.gnu.build-id NOTE 0000000000000190 000190 000024 00 A 0 0 4
[ 2] .hash HASH 00000000000001b8 0001b8 002a88 04 A 4 0 8
[ 3] .gnu.hash GNU_HASH 0000000000002c40 002c40 002910 00 A 4 0 8
[ 4] .dynsym DYNSYM 0000000000005550 005550 00a038 18 A 5 7 8
[ 5] .dynstr STRTAB 000000000000f588 00f588 006f5c 00 A 0 0 1
[ 6] .gnu.version VERSYM 00000000000164e4 0164e4 000d5a 02 A 4 0 2
[ 7] .gnu.version_r VERNEED 0000000000017240 017240 000110 00 A 5 7 8
[ 8] .rela.dyn RELA 0000000000017350 017350 0386a0 18 A 4 0 8
[ 9] .rela.plt RELA 000000000004f9f0 04f9f0 001cc8 18 AI 4 19 8
[10] .plt PROGBITS 00000000000516c0 0516c0 001350 10 AX 0 0 16
[11] .text PROGBITS 0000000000052a10 052a10 25b058 00 AX 0 0 8
[12] .rodata PROGBITS 00000000002ada68 2ada68 04664d 00 A 0 0 8
[13] .eh_frame PROGBITS 00000000002f40b8 2f40b8 000004 00 A 0 0 4
[14] .init_array INIT_ARRAY 00000000002fbc88 2f7c88 000008 08 WA 0 0 8
[15] .fini_array FINI_ARRAY 00000000002fbc90 2f7c90 000008 08 WA 0 0 8
[16] .data.rel.ro PROGBITS 00000000002fbc98 2f7c98 000138 00 WA 0 0 8
[17] .dynamic DYNAMIC 00000000002fbdd0 2f7dd0 000230 10 WA 5 0 8
[18] .data PROGBITS 00000000002fc000 2f8000 10b790 00 WA 0 0 8
[19] .got.plt PROGBITS 0000000000407790 403790 0009a8 08 WA 0 0 8
[20] .got PROGBITS 0000000000408138 404138 000998 08 WA 0 0 8
[21] .sdata PROGBITS 0000000000408ad0 404ad0 000008 00 WA 0 0 8
[22] .bss NOBITS 0000000000408ad8 404ad8 023c00 00 WA 0 0 8
[23] .comment PROGBITS 0000000000000000 404ad8 000022 01 MS 0 0 1
[24] .debug_aranges PROGBITS 0000000000000000 404afa 001c00 00 0 0 1
[25] .debug_info PROGBITS 0000000000000000 4066fa 1f390b 00 0 0 1
[26] .debug_abbrev PROGBITS 0000000000000000 5fa005 01caf8 00 0 0 1
[27] .debug_line PROGBITS 0000000000000000 616afd 273a49 00 0 0 1
[28] .debug_frame PROGBITS 0000000000000000 88a548 05c2b8 00 0 0 8
[29] .debug_str PROGBITS 0000000000000000 8e6800 028332 01 MS 0 0 1
[30] .debug_ranges PROGBITS 0000000000000000 90eb32 0089c0 00 0 0 1
[31] .symtab SYMTAB 0000000000000000 9174f8 037380 18 32 7723 8
[32] .strtab STRTAB 0000000000000000 94e878 0219d8 00 0 0 1
[33] .shstrtab STRTAB 0000000000000000 970250 000142 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
p (processor specific)
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x2f40bc 0x2f40bc R E 0x4000
LOAD 0x2f7c88 0x00000000002fbc88 0x00000000002fbc88 0x10ce50 0x130a50 RW 0x4000
DYNAMIC 0x2f7dd0 0x00000000002fbdd0 0x00000000002fbdd0 0x000230 0x000230 RW 0x8
NOTE 0x000190 0x0000000000000190 0x0000000000000190 0x000024 0x000024 R 0x4
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10
GNU_RELRO 0x2f7c88 0x00000000002fbc88 0x00000000002fbc88 0x000378 0x000378 R 0x1
Section to Segment mapping:
Segment Sections...
00 .note.gnu.build-id .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .plt .text .rodata .eh_frame
01 .init_array .fini_array .data.rel.ro .dynamic .data .got.plt .got .sdata .bss
02 .dynamic
03 .note.gnu.build-id
04
05 .init_array .fini_array .data.rel.ro .dynamic
由于 ELF 文件头本身就是平台无关的,使用任何架构的 readelf
都可以读,也不管你是原生工具链,还是交叉工具链。
出于人类常识,移植一个全新的架构也犯不着把有的没的都全部改一遍,为了改而改,这样反而增加适配的工作量;所以 section 名字也都是熟悉的。那么代码段就还是叫 .text
——文本!机器语言对机器而言就是“文本”。
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[11] .text PROGBITS 0000000000052a10 052a10 25b058 00 AX 0 0 8
这意思就是,这个库的代码段装载到从 0x52a10
开始的一块内存,内容从文件的第 0x52a10
字节(编号从 0 开始)起,长度有 0x25b058
字节。
马上来看一下!还记得老胡那一页指令格式的 PPT 吗?所有指令都定长 32 位,跟大多数 RISC 架构都一样,那么——
import struct
import sys
# one little-endian 32-bit word
INSN = struct.Struct('<I')
with open(sys.argv[1], 'rb') as fp:
fp.seek(0x52a10, 0)
text = fp.read(0x25b058)
# 先看 10 条指令
i = 0
while i < 40:
insn = INSN.unpack(text[i:i + 4])[0]
i += 4
print(f'{insn:08x}')
1c0076a4
02dca084
1c0076ac
02dc818c
58001984
1c0076ac
28def18c
40000d80
4c000180
03400000
很工整嘛!看上去我们猜的没错——与所有其他常见 RISC 指令集一样,LoongArch 的机器码在内存中,就是一堆原生字节序的整数。对 LoongArch 而言,就是一堆 32 位的小端序整数。像这前两条指令 1c0076a4 02dca084
,你如果直接拿十六进制编辑器打开文件,其实在那个位置存放的是 a4 76 00 1c 84 a0 dc 02
,每 4 个字节都是反过来的。
那么我们现在已经找到了整个代码段,借助老胡的 PPT 助攻,我们甚至不用自己统计分析指令格式了,真的爽!何况现在已知的指令格式就有 10 种,还可能有未知的(剧透:真的有!),自己分析还不知要到猴年马月……现在只需要搞明白哪些 opcode 是使用的哪种指令格式,含义是啥就好了。
接下来,终于可以进入真正指令的破译工作了!欲知后事如何,且听下回分解~