简单介绍移植 Valgrind for LoongArch 的过程。
引言
好久没更新了,目前基本完成了 Valgrind for LoongArch 的适配,补丁的整理参考了 riscv64。
项目仓库见 https://github.com/FreeFlyingSheep/valgrind-loongarch64,通过了除 drd/tests/pth_mutex_signal
以外的所有回归测试,且这个未通过项目是已知的公共问题。
向上游邮件列表发送了邮件,进一步跟进中,希望能早日合入上游。
LibVEX
我实际完成 LibVEX 的移植是在 Valgrind 主体的移植后的,当时考虑的是先在本地通过编译跑起来再说。 但理论上应该先完成 LibVEX 的移植。
前端
前端主要包括三个文件:VEX/priv/guest_loongarch64_defs.h
、VEX/priv/guest_loongarch64_helpers.c
和 VEX/priv/guest_loongarch64_toIR.c
。
前端的设计主要参考了 arm64 和 mips64,不过我把所有指令的解析都分拆在各个函数里。
指令的分类我参考了 QEMU 的 LoongArch 补丁,翻译的内容见汇总,解码部分每几位归为一组,目的是尽可能凑 switch
函数的跳转表。
对于不方便翻译为 Vex IR 的指令,采用函数调用的方式直接交给帮助函数处理,比如一些 crc
指令等。
后端
前端主要包括三个文件:VEX/priv/host_loongarch64_defs.h
、VEX/priv/host_loongarch64_isel.c
和 VEX/priv/host_loongarch64_defs.c
。
host_loongarch64_defs.h
包括了各种指令的标签(用枚举定义)和结构体定义,方便起见,我直接把指令标签的枚举值定义为指令的操作码。
host_loongarch64_isel.c
负责把所有 Vex IR 转换成 LoongArch 指令的结构体,具体内容见汇总。
host_loongarch64_defs.c
负责把 LoongArch 指令的结构体转换成二进制,发射出去。
其中后端设计的原子操作(主要是 CAS)我直接抄了内核的实现(arch/loongarch/kernel/cmpxchg.c
)。
汇总
用到的字节序:
Iend_LE
:LoongArch 只有小端序
用到的常量:
Ico_F32i
:主要用于表示浮点数1.0
Ico_F64i
:主要用于表示浮点数1.0
Ico_U16
:主要用于表示 16 位立即数Ico_U32
:主要用于表示 32 位立即数Ico_U64
:主要用于表示pc
寄存器和 64 位立即数Ico_U8
:主要用于表示ui5
、ui6
、sa2
、sa3
、msbw
、lsbw
、msbd
、lsbd
和 8 位立即数Ico_U1
:主要用于表示比较运算的结果
用到的类型:
Ity_F32
:主要用于浮点寄存器的低 32 位Ity_F64
:主要用于浮点寄存器Ity_I16
:主要用于整数寄存器的低 16 位Ity_I32
:主要用于整数寄存器的低 32 位Ity_I64
:主要用于整数寄存器Ity_I8
:主要用于整数寄存器的低 8 位和fcc
寄存器Ity_I1
:主要用于整数寄存器的低 1 位
用到的操作符(浮点指令基本都涉及 fcsr
的保存和恢复):
Iop_128HIto64
:return hi
Iop_128to64
:return lo
Iop_16Sto64
:ext.w.h dst, src
Iop_16Uto64
:slli.d dst, src, 48; srli.d dst, dst, 48
Iop_1Sto64
:slli.d dst, src, 63; srai.d dst, dst, 63
Iop_1Uto64
:andi dst, src, 0x1
Iop_1Uto8
:andi dst, src, 0x1
Iop_32Sto64
:slli.w dst, src, 0
Iop_32Uto64
:slli.d dst, src, 32; srli.d dst, dst, 32
Iop_32to8
:andi dst, src, 0xff
Iop_64HIto32
:srli.d dst, src, 32
Iop_64to32
:slli.d dst, src, 32; srli.d dst, dst, 32
Iop_64to8
:andi dst, src, 0xff
Iop_8Sto64
:ext.w.b dst, src
Iop_8Uto32
:andi dst, src, 0xff
Iop_8Uto64
:andi dst, src, 0xff
Iop_AbsF32
:fabs.s dst, src
Iop_AbsF64
:fabs.d dst, src
Iop_Add32
:add[i].w dst, src1, src2
Iop_Add64
:add[i].d dst, src1, src2
Iop_AddF32
:fadd.s dst, src1, src2
Iop_AddF64
:fadd.d dst, src1, src2
Iop_And32
:and[i] dst, src1, src2
Iop_And64
:and[i] dst, src1, src2
Iop_CasCmpNE32
:xor dst, src1, src2; sltu dst, $zero, dst
Iop_CasCmpNE64
:xor dst, src1, src2; sltu dst, $zero, dst
Iop_Clz32
:clz.w dst, src
Iop_Clz64
:clz.d dst, src
Iop_CmpEQ32
:xor dst, src1, src2; sltui dst, dst, 1
Iop_CmpEQ64
:xor dst, src1, src2; sltui dst, dst, 1
Iop_CmpF32
:fcmp.cond.s dst, src1, src2
Iop_CmpF64
:fcmp.cond.d dst, src1, src2
Iop_CmpLT32S
:slli.w src1, src1, 0; slli.w src2, src2, 0; slt dst, src1, src2
Iop_CmpLT32U
:slli.w src1, src1, 0; slli.w src2, src2, 0; sltu dst, src1, src2
Iop_CmpLE64S
:slt dst, src2, src1; nor dst, src, $zero
Iop_CmpLE64U
:sltu dst, src2, src1; nor dst, src, $zero
Iop_CmpLT64S
:slt dst, src1, src2
Iop_CmpLT64U
:sltu dst, src1, src2
Iop_CmpNE32
:xor dst, src1, src2; sltu dst, $zero, dst
Iop_CmpNE64
:xor dst, src1, src2; sltu dst, $zero, dst
Iop_Ctz32
:ctz.w dst, src
Iop_Ctz64
:ctz.d dst, src
Iop_DivF32
:fdiv.s dst, src1, src2
Iop_DivF64
:fdiv.d dst, src1, src2
Iop_DivModS32to32
:div.w lo, src1, src2; mod.w hi, src1, src2; slli.d hi, hi, 6; or dst, lo, hi
Iop_DivModS64to64
:div.d lo, src1, src2; mod.d hi, src1, src2
Iop_DivModU32to32
:div.wu lo, src1, src2; mod.wu hi, src1, src2; slli.d hi, hi, 6; or dst, lo, hi
Iop_DivModU64to64
:div.du lo, src1, src2; mod.du hi, src1, src2
Iop_DivS32
:div.w dst, src1, src2
Iop_DivS64
:div.wu dst, src1, src2
Iop_DivU32
:div.d dst, src1, src2
Iop_DivU64
:div.du dst, src1, src2
Iop_F32toF64
:fcvt.s.d dst, src
Iop_F32toI32S
:ftint.w.s dst, src
Iop_F32toI64S
:ftint.l.s dst, src
Iop_F64toF32
:fcvt.s.d dst, src
Iop_F64toI32S
:ftint.w.d dst, src
Iop_F64toI64S
:ftint.l.d dst, src
Iop_I32StoF32
:ffint.s.w dst, src
Iop_I32StoF64
:ffint.d.w dst, src
Iop_I64StoF32
:ffint.s.l dst, src
Iop_I64StoF64
:ffint.d.l dst, src
Iop_MAddF32
:fmadd.s dst, src1, src2, src3
Iop_MAddF64
:fmadd.d dst, src1, src2, src3
Iop_MSubF32
:fmsub.s dst, src1, src2, src3
Iop_MSubF64
:fmsub.d dst, src1, src2, src3
Iop_MaxNumAbsF32
:fmaxa.s dst, src1, src2
Iop_MaxNumAbsF64
:fmaxa.d dst, src1, src2
Iop_MinNumAbsF32
:fmina.s dst, src1, src2
Iop_MinNumAbsF64
:fmina.d dst, src1, src2
Iop_MaxNumF32
:fmax.s dst, src1, src2
Iop_MaxNumF64
:fmax.d dst, src1, src2
Iop_MinNumF32
:fmin.s dst, src1, src2
Iop_MinNumF64
:fmin.d dst, src1, src2
Iop_MulF32
:fmul.s dst, src1, src2
Iop_MulF64
:fmul.d dst, src1, src2
Iop_MullS32
:mulw.d.w dst, src1, src2
Iop_MullS64
:mul.d lo, src1, src2; mulh.d hi, src1, src2
Iop_MullU32
:mulw.d.wu dst, src1, src2
Iop_MullU64
:mul.d lo, src1, src2; mulh.du hi, src1, src2
Iop_NegF32
:fneg.s dst, src
Iop_NegF64
:fneg.d dst, src
Iop_Not32
:nor dst, src, $zero
Iop_Not64
:nor dst, src, $zero
Iop_Or1
:or dst, src1, src2
Iop_Or32
:or[i] dst, src1, src2
Iop_Or64
:or[i] dst, src1, src2
Iop_RSqrtF32
:frsqrt.s dst, src
Iop_RSqrtF64
:frsqrt.d dst, src
Iop_ReinterpF32asI32
:movfr2gr.s dst, src
Iop_ReinterpF64asI64
:movfr2gr.d dst, src
Iop_ReinterpI32asF32
:movgr2fr.w dst, src
Iop_ReinterpI64asF64
:movgr2fr.d dst, src
Iop_RoundF32toInt
:frint.s dst, src
Iop_RoundF64toInt
:frint.d dst, src
Iop_Sar32
:sra[i].w dst, src1, src2
Iop_Sar64
:sra[i].d dst, src1, src2
Iop_Shl32
:sll[i].w dst, src1, src2
Iop_Shl64
:sll[i].d dst, src1, src2
Iop_Shr32
:srl[i].w dst, src1, src2
Iop_Shr64
:srl[i].d dst, src1, src2
Iop_SqrtF32
:fsqrt.s dst, src
Iop_SqrtF64
:fsqrt.s dst, src
Iop_Sub32
:sub.w dst, src1, src2
Iop_Sub64
:sub.d dst, src1, src2
Iop_SubF32
:fsub.s dst, src1, src2
Iop_SubF64
:fsub.d dst, src1, src2
Iop_Xor32
:xor[i] dst, src1, src2
Iop_Xor64
:xor[i] dst, src1, src2
用到的表达式:
Iex_Binop
:用于表示二元操作符和对应的操作数Iex_Const
:用于表示常量Iex_Get
:用于读寄存器Iex_ITE
:用于表示if-else
操作Iex_Load
:用于读内存Iex_Qop
:用于表示四元操作符和对应的操作数Iex_RdTmp
:用于表示读取临时变量Iex_Triop
:用于表示三元操作符和对应的操作数Iex_Unop
:用于表示一元操作符和对应的操作数
用到的跳转方式:
Ijk_Boring
:用于表示普通的跳转Ijk_ClientReq
:用于表示需要处理客户端请求(Valgrind 专用)Ijk_SigFPE_IntDiv
:用于表示当前指令触发SIGFPE
异常,且是整数除 0 异常Ijk_SigFPE_IntOvf
:用户表示当前指令触发SIGFPE
异常,且是整数溢出异常Ijk_INVALID
:用于表示无效(libVEX 本身出现错误)Ijk_InvalICache
:用于表示需要使指令缓存无效(Valgrind 专用)Ijk_NoDecode
:用于表示解码失败Ijk_NoRedir
:用于表示跳转到未重定向到地址(Valgrind 专用)Ijk_SigBUS
:用于表示当前指令触发SIGBUS
异常Ijk_SigILL
:用于表示当前指令触发SIGILL
异常Ijk_SigSEGV
:用于表示当前指令触发SIGSEGV
异常Ijk_SigSYS
:用于表示当前指令触发SIGSYS
异常Ijk_SigTRAP
:用于表示当前指令触发SIGTRAP
异常Ijk_Sys_syscall
:用于表示需要进行系统调用
用到的语句:
Ist_CAS
:用于原子操作(Compare And Swap)Ist_Exit
:用于表示退出Ist_LLSC
:用于原子操作(ll
/sc
)Ist_MBE
:用于表示内存屏障Ist_Put
:用于表示写寄存器Ist_Store
:用于表示写内存Ist_WrTmp
:用于表示给临时变量赋值
Valgrind 特殊指令:
|
|
公共框架添加的内容:
Ist_MBE
:添加表示指令屏障的功能Iop_LogBF32
:flogb.s dst, src
Iop_LogBF64
:flogb.d dst, src
Iop_ScaleBF32
:fscaleb.s dst, src1, src2
Iop_ScaleBF64
:fscaleb.s dst, src1, src2
除此以外,还有其他 Valgrind 工具(如 memcheck)会用到一些专有操作符(如 Iop_CmpNEZ8
),也需要在后端翻译,此处略去。
Valgrind
Valgrind 主体的移植第一步是解决编译问题,几乎所有需要修改的地方都有 #error
提示,顺着编译报错加入 LoongArch 的 #ifdef
,不确定的地方先写 /* TODO */
。
在顺利通过了编译后,再开始分模块完善代码,下面介绍几个关键模块的移植思路。
gdbserver
gdbserver 部分的代码主要位于 coregrind/m_gdbserver/valgrind-low-loongarch64.c
。
这部分代码参考了 GDB 的实现(gdb/gdbserver/linux-loongarch-low.c
),因为新版本 GDB 的相关代码已经重构过,所以要适当调整(不能照抄,有点难受)。
测试的时候使用 --vgdb=yes --vgdb-error=0
参数。
sigframe
信号栈部分的代码主要位于 coregrind/m_sigframe/sigframe-loongarch64-linux.c
。
这部分代码参考内核信号栈的实现(arch/loongarch/kernel/signal.c
)。
目前只考虑整数寄存器,暂不实现浮点寄存器和二进制翻译寄存器、向量寄存器的存取。
dispatch
分派部分的代码主要位于 coregrind/m_dispatch/dispatch-loongarch64-linux.S
。
这部分代码参考 mips64 的实现,最大的区别是要加上指令屏障。
cache
缓存部分的代码主要位于 coregrind/m_cache.c
。
这部分代码参考内核 CPU 探测部分(arch/loongarch/mm/cache.c
),利用 cpucfg
指令实现了用户态下对 CPU 缓存属性的读取。
machine
机器探测部分的代码主要位于 coregrind/m_machine.c
。
似乎可以借助 cpucfg
指令,但这样有一个问题,硬件支持但内核不支持的属性,应用软件同样无法使用。
所以参考 mips 读取 /proc/cpuinfo
来完成相应设置。
踩坑
指令屏障
执行同一个程序,有时候会 SIGILL,大部分时候又能正常运行,用 GDB 追踪时却总是好的。
索性在内核添加打印,发现触发 SIGILL 的指令编码永远是 0
。
后来想了想,可能是需要添加指令屏障,在 dispatch 模块跳转到生成的指令前加了 ibar
指令,之后果然好了。
32 位除法结果不对
测试发现 32 位除法指令的结果是不对的,但我怎么都觉得自己翻译代码没写错,一脸懵逼。
手册上在除法指令介绍完以后只写了一句话,描述如果超过 32 位,则结果为无意义的任何值。
我一开始理解为就是高 32 位必须为 0
,但并不是这个意思。
单独验证除法指令,实际 CPU 执行时,对于正数,高 32 位必须为 0
,而对于负数,必须高位全 1
。
因为第 32 位为 1
且高 32 位为 0
被解释为正数,而这个数超了 32 位整数的范围。
一个简单粗暴的解决方案是把 32 位除法指令的操作数统一进行符号扩展。
地址解析问题
经过不停的修改测试,好不容易把静态链接的小程序跑通了,但一运行动态链接程序就段错误,让人费解。
由于是挂在动态链接器里,所以比较难追踪,为此我甚至写了不少脚本去测试单指令的正确性,以及比较不同日志里面涉及的指令,见脚本。
某天突然发现了端倪,有大量 ld
指令的立即数是很大的负数,这种现象不正常。
再次检查 VEX/priv/host_loongarch64_isel.c
中的代码,ld
指令立即数的最高位是符号位,因此解析地址时(iselIntExpr_AMode_wrk()
),超过 11 位的立即数就应该走 ldx
指令,而我判断的时候用的是 0xfff
,改成 0x7ff
就解决了问题。
来自 C 库的警告
使用 memcheck 跑动态链接程序的时候,发现一万个警告,而在 x86 下运行是没这些警告的。
后来发现可以通过 *.supp
文件忽略 C 库的警告,于是我暂时偷懒直接在 glibc-2.X.supp.in
中把 */libc.so*
和 */ld-linux-loongarch-*.so*
文件产生的警告给忽略了。
|
|
在完成了一些验证后,我发现报错的来源主要是 sc.w
指令,我怀疑是我模拟的有问题。
在逐步排查所有 exit
的地方后,发现问题大概是出自 guest_LLSC_DATA
和 data
的比较上,因为 guest_LLSC_DATA
是一个 64 位的变量,我把 32 位的 data
也转成 64 位后, sc
指令产生的报错就消失了。
其他报错来自于 C 位域,反汇编后发现和 bstrins
指令有关,推测是我偷懒用 C 语言函数模拟,导致 memcheck 不能正确跟踪位的情况,于是老老实实用多条 Vex IR 指令模拟,之后发现 memcheck 关于 C 库的报错全没了,太爽了。
除了 memcheck,helgrind 也有来自 C 库的警告,最终发现是需要在 coregrind/m_redir.c
中添加动态链接库的名字。
结构体要及时同步
运行 valgrind --tool=none gcc --version
结果段错误了,用 GDB 追踪发现系统调用传递的参数都错位了。
排查了半天发现是新版本内核结构体变动了,没及时跟上(内核还没进社区,每一版改动可能较大),这蛋疼的 Valgrind 内核接口模块,每次都要手动去更新。
GDB
每次测试 Valgrind 的 gdbserver 功能都提示 Remote 'g' packet reply is too long
。
查看社区版 GDB 源码后发现尽管写了代码,但竟然默认不支持浮点寄存器(gdb/arch/loongarch.c
),我把 coregrind/m_gdbserver/valgrind-low-loongarch64.c
中浮点寄存器代码注释掉后就好了。
最后改用了内部完整版的 GDB 进行测试,把浮点寄存器加回去了。
VDSO
每次在中文环境下执行 valgrind --tool=none ls -l
就会段错误,一直以为是指令翻译有问题,追了好久发现是信号处理的时候出了问题。
中文环境下 valgrind ls -l
默认客户栈不够,此时内核发送 SIGSEGV
,Valgrind 信号处理函数会调用 mmap()
系统调用扩展栈,之后返回的时候到了 VDSO 的 sigreturn()
函数。
这时候问题就来了,Valgrind 的地址空间管理器默认会移除 VDSO 的映射,这时候跳转到的地址是非法的,因此再次触发 SIGSEGV
,程序退出。
而高版本内核不支持 SA_RESTORER
,因此没法向其他架构一样用自己的 my_sigreturn()
函数。
后来追踪历史记录发现在 coregrind/m_initimg/initimg-linux.c
中不取消映射就行了(添加 #ifdef
)。
LLSC
LoongArch 下的 ll
/sc
指令对,中间不能有其他访存指令,不然一定会失败。
而要保证 Valgrind 不在 ll
/sc
指令对中使用其他访存指令是非常难的,目前并没有架构实现。
这个问题导致了用到 LLSC 的程序几乎都死循环了,于是我实现了 fallback 版本的 LLSC(即用软件模拟),并强制启用它。
AM*
原子指令
有时候遇到 AM*
原子指令的程序会死循环,而稍微改改前端代码,增加或者删掉几个临时 Vex IR 变量又突然好了,非常迷。
在 GDB 里单指令跟踪,发现最后发射的 ll
/sc
指令对总是失败,查看寄存器内容是 ll.w
符号扩展了,而比较的时候,另一个操作数还是 32 位的,必定失败。
对另一个操作数进行符号扩展,总算解决了这个问题。
接收 SIGINT
后直接报错
在部分多线程程序中按 ctrl + c
,会导致 coregrind/m_signals.c
中 async_signalhandler()
函数的 vg_assert(tst->status == VgTs_WaitSys);
失败。
搜索历史记录,有不少架构也修过这个问题,所以一开始怀疑是哪里需要加东西,但怎么都找不到。 后来追踪 Valgrind 大锁和信号的 mask,发现该阻塞的地方没阻塞,这一定是哪里设置出了问题。
阻塞信号用的是 rt_sigprocmask()
系统调用,查找用到这个系统调用的地方,发现 coregrind/m_syswrap/syscall-loongarch64-linux.S
最可疑。
比照 arm64,果然抄错了一个地方,第二次重新阻塞时应该用 postmask
而不是 syscall_mask
。
修改后程序能正常处理 SIGINT
,总算告一段落。
加载立即数
为了优化大量的加载立即数指令,根据立即数大小分为三种情况:
|
|
使用一条指令加载:
|
|
使用两条指令加载:
|
|
使用四条指令加载:
|
|
在验证 pcaddu12i
和 pcalau12i
指令时发现问题,高位应该为 0
的情况变成全 1
了。
追踪发现是加载立即数是,希望加载一个第 32 位为 1
的 64 位立即数,但被误判为了使用两条指令去加载 32 位立即数。
解决办法是修改 imm < 0xffffffff
为 imm < 0x80000000
,确保这种情况下使用四条指令去加载。
movcf2gr
和 movcf2fr
movcf2gr
和 movcf2fr
指令会清空高位,手册没写明。
clone3()
系统调用
Valgrind 并未实现 clone3()
系统调用相关的代码,因此我参考其他架构,直接在 coregrind/m_syswrap/syswrap-loongarch64-linux.c
的系统调用表里,把 clone3()
设置为 sys_ni_syscall
。
脚本
记录一些方便自己测试、调试的小脚本。本着能用就行的原则,写得很烂(其实是太菜,逃
验证单指令正确性
为了绕开 glibc,自己实现打印、退出函数(通过系统调用),程序开始执行时保存所有寄存器,这部分用汇编实现:
|
|
之后跳转到 C 函数去打印保存的寄存器:
|
|
只需要把 nop
替换为需要验证的非转移指令,上述代码就可以打印运行指令后的寄存器状态。
于是之后借助一个 python 脚本来实现不同指令的填充和验证工作:
|
|
同理,对浮点指令的测试只需要稍微修改脚本:
|
|
在 dump.S
前加入对 fcsr
寄存器的修改:
|
|
使用的数据文件 data.c
如下(抄的 mips 测试案例):
|
|
对于 show.c
,只检查 f0
和 fcsr
即可:
|
|
借助这个脚本我发现了大量翻译指令时的笔误……
比对文件输出
当时遇到一个问题,发现运行动态链接程序会发生段错误,而静态链接的版本能正常跑过。
我希望能通过比对运行动态和静态链接两个版本的 Valgrind 日志,来定位是哪条或者哪些指令可能出了问题。 于是顺手写了一个 golang 的比较程序:
|
|
检查笔误、遗漏
检查简单的笔误,以及博文里指令是否写全:
|
|