LoongArch 研究小记

阅读量    63522 |

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

作者: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 的反编译器,后续合适的时机可能会公开。

 

参考

LoongArch64 指令手册

LoongArch 指令集介绍.pdf

LoongArch 官博

龙芯 github 地址

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多