作者:xiongxiao (395984722@qq.com), jiayy (chengjia4574@gmail.com)
LoongArch
目前世界上主要的指令集架构有 MIPS, X86, Power, Alpha, ARM 等,除了 ARM 是英国的其余都是美国的。国内的芯片厂商龙芯,君正,兆芯,海光,申威,飞腾,海思,展讯,华芯通等购买相应授权并开发相应芯片产品,这就是目前芯片市场的情况,可以说脖子被卡得死死的。
2021.04.30,龙芯自主指令系统LoongArch基础架构手册正式发布 ,号称从顶层架构,到指令功能和 ABI 标准等,全部自主设计,不需国外授权。2021.07.23, 基于自主指令集 LA 架构的 新一代处理器龙芯3A5000正式发布 ,据称 spec 2006评分达到26分,接近30分的一代锐龙。
我们小组及时跟进研究了 LA 的手册,并在 3A5000 设备上开发了相应的产品。在这过程中发现网上对这一新生事物缺乏资料(除了官方的),遂写了本篇小记。
inline Hook
其中一个任务是实现 LA 上的 inline hook 。指令手册主要参考:
- 第二章 基础整数指令, 解释指令格式和功能
- 附录B 指令码一览, 指令的二进制编码方式
寄存器
基础整数指令涉及的寄存器包括通用寄存器(General-purpose Register,简称 GR) 和 程序记数寄存器(Program Counter,简称PC)
通用寄存器GR有32个,记为r0~r31, 其中 0 号寄存器r0的值恒为 0。
GR 的位宽记做 GRLEN。LA32 32bit, LA64 64bit。
在标准的龙芯架构应用程序二进制接口(Application Binary Interfac, 简称ABI) 中,r1 固定作为存放函数调用返回地址的寄存器。
其中GR包括 r0 ... r31 共32个
PC 只有1个,记录当前指令的地址。
PC 寄存器不能被指令直接修改,只能被转移指令、例外陷入和例外返回指令间接修改。
可以作为一些非转移类指令的源操作数直接读取。
(以上内容全部摘自指令手册)
补充:
根据LoongArch ABI,寄存器功能的更细的划分如下:
R0 : 永 远 为0
R1 : ra 返 回 地 址
R2 : tp , 线 程 指 针
R3 : sp , 栈 指 针
R4−R11: 参 数a0−a7 , a0/a1 返 回
R12−R20 : t0−t8 临 时 寄 存 器
R21 : r e s e r v e
R22 : fp
R23−R31 : s0−s8 c a l l e e
指令
这里通过BEQ指令说明如何查询手册,快速获得这条指令相关的信息
# 在附录中可以找到指令的编码
BEQ rj, rd, offs | 0 1 0 1 1 0 offs[15:0] rj rd
# 在第二章可以找到指令的功能解释以及编码含义
BEQ 将通用寄存器 rj 和通用寄存器 rd 的值进行比较,如果两者相等则跳转到目标地址,否则不跳转
if GR[rj] == GR[rd] :
PC = PC + SignExtend(offs16, 2'b0}, GRLEN)
伪代码中 SignExtend(offs16, 2’b0}, GRLEN) 的含义是offs16 左移两位,然后符号扩展到GRLEN(LA64下 即64位)
关于符号扩展Wiki,C实现如下:
// 依赖 >> 符号本身就是符号扩展的特性,可以简单实现为
long sign_extend(long off, int bits){
return ((off << (64 - bits)) >> (64 - bits));
}
// 不依赖 << 符号
#include <stdio.h>
long sign_extend(long off, unsigned int bits){
long sign_mask = 1UL << (bits - 1); // bit[bits - 1] 为 1,其他位全部为 0
long pos_mask = (1UL << bits) - 1; // bit[0:bits] 全部为 1, bit[bits: 63] 全部为0
long neg_mask = ~((1UL << bits) - 1); // bit[0:bits] 全部为 0, bit[bits: 63] 全部为1
if(off & sign_mask){
// 符号位为 1, 保证扩展后的高位全部为 1
return off | neg_mask;
}else{
// 符号位为 0, 保证扩展后的高位全部为 0
return off & pos_mask;
}
}
int main(){
printf("0x%lx\n", sign_extend(0x80, 8)); // 0xffffffffffffff80
printf("0x%lx\n", sign_extend(0x80, 9)); // 0x80
}
PC 相对寻址指令替换
inline hook 的主要工作之一就是修复这类指令,即计算出正确的地址,然后通过其他指令替换
LoongArch64 中的PC相对寻址指令如下:
算数运算指令
PCADDI rd, si20 | 0 0 0 1 1 0 0 si20 rd
PCALAU12I rd, si20 | 0 0 0 1 1 0 1 si20 rd
PCADDU12I rd, si20 | 0 0 0 1 1 1 0 si20 rd
PCADDU18I rd, si20 | 0 0 0 1 1 1 1 si20 rd
转移指令
BEQZ rj, offs | 0 1 0 0 0 0 offs[15:0] rj offs[20:16]
BNEZ rj, offs | 0 1 0 0 0 1 offs[15:0] rj offs[20:16]
BCEQZ cj, offs | 0 1 0 0 1 0 offs[15:0] 0 0 cj offs[20:16]
BCNEZ cj, offs | 0 1 0 0 1 0 offs[15:0] 0 1 cj offs[20:16]
# JIRL rd, rj, offs | 0 1 0 0 1 1 offs[15:0] rj rd (唯一一个不是PC相对寻址的转移指令)
B offs | 0 1 0 1 0 0 offs[15:0] offs[25:16]
BL offs | 0 1 0 1 0 1 offs[15:0] offs[25:16]
BEQ rj, rd, offs | 0 1 0 1 1 0 offs[15:0] rj rd
BNE rj, rd, offs | 0 1 0 1 1 1 offs[15:0] rj rd
BLT rj, rd, offs | 0 1 1 0 0 0 offs[15:0] rj rd
BGE rj, rd, offs | 0 1 1 0 0 1 offs[15:0] rj rd
对这两类的指令替换方案如下:
pcaddi [target_reg], si20 替换为:
PCADDI r17, 12/4 # 将 pc + 12 存放到 r17 临时寄存器
LD.D [target_reg], r17, 0 # 取出 r17 地址处的 8 个字节保存到 target_reg
B 12/4 # 跳过存放地址的8个字节,即 pc += 12,由于指令会对偏移移位,所以要12/4
IMM[ 0: 31] # 基于原指令pc 计算得到的结果低32bit
IMM[32: 63] # 基于原指令pc 计算得到的结果高32bit
b offs 替换为:
PCADDI R17, 12/4 # 将 pc + 12 存放到 r17 临时寄存器
LD.D R17, R17, 0 # 取出 r17 地址处的 8 个字节保存到 r17
JIRL R0, R17, 0 # 跳转到 r17 保存的地址处
TO_ADDR[0 : 31] # 基于原指令pc 计算得到的跳转地址低32bit
TO_ADDR[32: 63] # 基于原指令pc 计算得到的跳转地址高32bit
# 条件跳转类的替换方式如下:
BEQ rj, rd, offs 替换为:
BNE rj, rd, 24/4
PCADDI R17, 12/4
LD.D R17, R17, 0
JIRL R0, R17, 0
TO_ADDR[0 : 31]
TO_ADDR[32: 63]
还有其他若干种需要处理的指令,这里不一一赘述。
r1寄存器
有时函数栈的切换不会把返回值压栈,而是直接使用r1寄存器
经测试,当一个函数没有调用子函数的时候,不会把 r1 压栈
开启gcc 编译优化也会省去压栈操作
// main.c
int func1(int a, int b){
return a + b;
}
int func2(int a, int b){
return func1(a, b) + 10;
}
int main(int argc, char *argv[]){
func1(100, 200);
func2(100, 200);
}
$ gcc main.c -g
$ gdb a.out
(gdb) disassemble func1
Dump of assembler code for function func1:
0x0000000120000650 <+0>: addi.d $r3,$r3,-32(0xfe0)
0x0000000120000654 <+4>: st.d $r22,$r3,24(0x18)
0x0000000120000658 <+8>: addi.d $r22,$r3,32(0x20)
0x000000012000065c <+12>: move $r13,$r4
0x0000000120000660 <+16>: move $r12,$r5
0x0000000120000664 <+20>: slli.w $r13,$r13,0x0
0x0000000120000668 <+24>: st.w $r13,$r22,-20(0xfec)
0x000000012000066c <+28>: slli.w $r12,$r12,0x0
0x0000000120000670 <+32>: st.w $r12,$r22,-24(0xfe8)
0x0000000120000674 <+36>: ld.w $r13,$r22,-20(0xfec)
0x0000000120000678 <+40>: ld.w $r12,$r22,-24(0xfe8)
0x000000012000067c <+44>: add.w $r12,$r13,$r12
0x0000000120000680 <+48>: move $r4,$r12
0x0000000120000684 <+52>: ld.d $r22,$r3,24(0x18)
0x0000000120000688 <+56>: addi.d $r3,$r3,32(0x20)
0x000000012000068c <+60>: jirl $r0,$r1,0
End of assembler dump.
(gdb) disassemble func2
Dump of assembler code for function func2:
0x0000000120000690 <+0>: addi.d $r3,$r3,-32(0xfe0)
0x0000000120000694 <+4>: st.d $r1,$r3,24(0x18)
...
0x00000001200006d8 <+72>: ld.d $r1,$r3,24(0x18)
0x00000001200006dc <+76>: ld.d $r22,$r3,16(0x10)
0x00000001200006e0 <+80>: addi.d $r3,$r3,32(0x20)
0x00000001200006e4 <+84>: jirl $r0,$r1,0
End of assembler dump.
$ gcc main.c -O2 -g
$ gdb a.out
Dump of assembler code for function func1:
0x0000000120000658 <+0>: add.w $r4,$r4,$r5
0x000000012000065c <+4>: jirl $r0,$r1,0
End of assembler dump.
(gdb) disassemble func2
Dump of assembler code for function func2:
0x0000000120000660 <+0>: add.w $r4,$r4,$r5
0x0000000120000664 <+4>: addi.w $r4,$r4,10(0xa)
0x0000000120000668 <+8>: jirl $r0,$r1,0
End of assembler dump.
如果 hook 过程里遇到目标函数是这种情况的,也要特殊处理。
用户态Hook
简单实现,不处理pc相对寻址的情况
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#define JUMP_CODE_SIZE 20
int (*func_ptr)(int, int, int);
int func(int a, int b, int c){
if(a == 0){
return 0;
}
printf("%s-%d: %d\n", __func__, __LINE__, a+b+c);
return a+b+c;
}
int hook_handler(int a, int b, int c){
printf("%s-%d: %d, %d, %d\n", __func__, __LINE__, a, b, c);
func_ptr(a, b, c);
return 0;
}
static char *do_jump(char *from, char *to) {
int rd, rj, off;
int inst_pcaddi, inst_jirl, inst_ld_d;
int to_addr_low, to_addr_high;
// PCADDI rd, si20 | 0 0 0 1 1 0 0 si20 rd
rd = 17;
off = 12 >> 2;
inst_pcaddi = 0x0c << (32 - 7) | off << 5 | rd ;
// LD.D rd, rj, si12 | 0 0 1 0 1 0 0 0 1 1 si12 rj rd
rd = 17;
rj = 17;
off = 0;
inst_ld_d = 0xa3 << 22 | off << 10 | rj << 5 | rd ;
// JIRL rd, rj, offs | 0 1 0 0 1 1 offs[15:0] rj rd
rd = 0;
rj = 17;
off = 0;
inst_jirl = 0x13 << 26 | off << 10 | rj << 5| rd;
to_addr_low = (int)((long)to & 0xffffffff);
to_addr_high = (int)((long)to >> 32);
*(int *)from = inst_pcaddi;
*(int *)(from + 4) = inst_ld_d;
*(int *)(from + 8) = inst_jirl;
*(int *)(from + 12) = to_addr_low;
*(int *)(from + 16) = to_addr_high;
return from + 20;
}
#define PAGE_MASK (~(page_size-1))
void post_hook(void *target, void *handler){
int page_size = sysconf(_SC_PAGE_SIZE);
int stolen = JUMP_CODE_SIZE;
char *trampoline = mmap(NULL, 128, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
// turn [ trampoline pointer ] into [ hook target function pointer ]
func_ptr = (int (*)(int, int, int))trampoline;
// copy changed inst [ target: target+stolen ]
memcpy(trampoline, target, stolen);
// jump from [ trampoline + stolen ] to [ target + stolen ]
do_jump(trampoline+stolen, target+stolen);
// [ target ] jump to [ handler ]
// 没有这个mprotect调用会出现段错误
mprotect((void*)((long)target & PAGE_MASK), page_size, PROT_READ|PROT_WRITE|PROT_EXEC);
do_jump(target, handler);
};
int main(int argc, char *argv[]){
post_hook((void *)func, (void *)hook_handler);
func(100, 200, 300);
return 0;
}
内核态Hook
我们实现了完整的处理各种异常条件的内核 LA inlineHook, 暂不公开
反编译器
有LoongArch64 机器的情况下,直接用gdb就可以做到
用一个简单的脚本实现:
#!/usr/bin/env python3
import os
opcodes = ",".join(hex(i) for i in [0x28c0208c, 0x28c0c18c, 0x24000d8c, 0x0348018c, 0x44008980])
c_code = """
int opcodes[] = { %s };
void main() { ((void (*)() )opcodes)(); }
""" % opcodes
with open("main.c", 'w') as f:
f.write(c_code)
os.system("gcc main.c -g")
os.system("gdb -batch -ex 'file a.out' -ex 'disassemble/rs opcodes'")
os.system("rm main.c a.out")
效果如下:
$ ./t.py
Dump of assembler code for function opcodes:
0x0000000120008000 <+0>: 8c 20 c0 28 ld.d $r12,$r4,8(0x8)
0x0000000120008004 <+4>: 8c c1 c0 28 ld.d $r12,$r12,48(0x30)
0x0000000120008008 <+8>: 8c 0d 00 24 ldptr.w $r12,$r12,12(0xc)
0x000000012000800c <+12>: 8c 01 48 03 andi $r12,$r12,0x200
0x0000000120008010 <+16>: 80 89 00 44 bnez $r12,136(0x88) # 0x120008098
End of assembler dump.
在没有 LoongArch64 机器的情况下,需要用软件(反编译器)实现 LA 指令的反编译,为了达到这个目的,我们正在开发支持 LA 的反编译器,后续合适的时机可能会公开。
发表评论
您还未登录,请先登录。
登录