chaos-mesh on mips64
问题起因
编译chaos-mesh v1.0.3 报错
[root@localhost chaos-mesh]# make chaosdaemon
GO15VENDOREXPERIMENT="1" CGO_ENABLED=1 GOOS="" GOARCH="" go build -ldflags '-s -w -X 'github.com/chaos-mesh/chaos-mesh/pkg/version.buildDate=2021-02-20T03:20:41Z' -X 'github.com/chaos-mesh/chaos-mesh/pkg/version.gitCommit=28de66e58ef60056b1f1966527fe85cc2396d670' -X 'github.com/chaos-mesh/chaos-mesh/pkg/version.gitVersion=chart-0.3.3'' -o bin/chaos-daemon ./cmd/chaos-daemon/main.go
# github.com/chaos-mesh/chaos-mesh/pkg/ptrace
pkg/ptrace/ptrace_linux.go:210:61: p.backupRegs.Rip undefined (type *syscall.PtraceRegs has no field or method Rip)
pkg/ptrace/ptrace_linux.go:225:61: p.backupRegs.Rip undefined (type *syscall.PtraceRegs has no field or method Rip)
pkg/ptrace/ptrace_linux.go:261:6: regs.Rax undefined (type syscall.PtraceRegs has no field or method Rax)
pkg/ptrace/ptrace_linux.go:265:8: regs.Rdi undefined (type syscall.PtraceRegs has no field or method Rdi)
pkg/ptrace/ptrace_linux.go:267:8: regs.Rsi undefined (type syscall.PtraceRegs has no field or method Rsi)
pkg/ptrace/ptrace_linux.go:269:8: regs.Rdx undefined (type syscall.PtraceRegs has no field or method Rdx)
pkg/ptrace/ptrace_linux.go:271:8: regs.R10 undefined (type syscall.PtraceRegs has no field or method R10)
pkg/ptrace/ptrace_linux.go:273:8: regs.R8 undefined (type syscall.PtraceRegs has no field or method R8)
pkg/ptrace/ptrace_linux.go:275:8: regs.R9 undefined (type syscall.PtraceRegs has no field or method R9)
pkg/ptrace/ptrace_linux.go:289:61: p.backupRegs.Rip undefined (type *syscall.PtraceRegs has no field or method Rip)
pkg/ptrace/ptrace_linux.go:289:61: too many errors
查看对应的目录
root@sjt-pc:/wk/github.com/chaos-mesh/chaos-mesh/pkg/ptrace# tree
.
├── cwrapper_linux.go
├── ptrace_linux.go
└── ptrace_linux_test.go
参照 golang 的编译约束,这些文件仅在 linux 下生效,但是很明显在 mips 下报错了,说明该文件并不仅仅只有操作系统约束,还应该是架构约束,查看最新版本的 chaos-mesh 对应目录,果然已经加上了架构约束。
root@sjt-pc:/wk/github.com/chaos-mesh/chaos-mesh/pkg/ptrace# tree
.
├── cwrapper_linux.go
├── ptrace_linux_amd64.go
└── ptrace_linux_test.go
那么我们的问题就是写出 ptrace_linux_mips64le.go。
定位
以下分析仍然针对v1.0.3 版本,分析源码,找到这个文件中的与架构有关的部分,我将其分为四个部分。
1. Ptrace 系统调用相关寄存器
我们找到两段代码,可以看到 backupRegs 实际上是 *syscall.PtraceRegs
。并且使用了其中的几个变量例如 Rip,Rax
等等。我们需要比对一下 amd64 和 mips64le 对这二者的实现有什么区别。
func (p *TracedProgram) Syscall(number uint64, args ...uint64) (uint64, error) {
...
var regs syscall.PtraceRegs
regs.Rax = number
for index, arg := range args {
// All these registers are hard coded for x86 platform
if index == 0 {
regs.Rdi = arg
} else if index == 1 {
regs.Rsi = arg
} else if index == 2 {
regs.Rdx = arg
} else if index == 3 {
regs.R10 = arg
} else if index == 4 {
regs.R8 = arg
} else if index == 5 {
regs.R9 = arg
} else {
return 0, fmt.Errorf("too many arguments for a syscall")
}
}
...
return regs.Rax, p.Restore()
}
2. PC 寄存器
func (p *TracedProgram) Restore() error {
err := syscall.PtraceSetRegs(p.pid, p.backupRegs)
if err != nil {
return errors.WithStack(err)
}
_, err = syscall.PtracePokeData(p.pid, uintptr(p.backupRegs.Rip), p.backupCode)
if err != nil {
return errors.WithStack(err)
}
return nil
}
3. 两个系统调用号
func (p *TracedProgram) WriteSlice(addr uint64, buffer []byte) error {
...
// process_vm_writev syscall number is 311
_, _, errno := syscall.Syscall6(311, uintptr(p.pid), uintptr(unsafe.Pointer(&localIov)), uintptr(1), uintptr(unsafe.Pointer(&remoteIov)), uintptr(1), uintptr(0))
...
}
func (p *TracedProgram) ReadSlice(addr uint64, size uint64) (*[]byte, error) {
...
// process_vm_readv syscall number is 310
_, _, errno := syscall.Syscall6(310, uintptr(p.pid), uintptr(unsafe.Pointer(&localIov)), uintptr(1), uintptr(unsafe.Pointer(&remoteIov)), uintptr(1), uintptr(0))
...
}
4. 两个指令的机器码
func (p *TracedProgram) JumpToFakeFunc(originAddr uint64, targetAddr uint64) error {
instructions := make([]byte, 16)
// mov rax, targetAddr;
// jmp rax ;
instructions[0] = 0x48
instructions[1] = 0xb8
binary.LittleEndian.PutUint64(instructions[2:10], targetAddr)
instructions[10] = 0xff
instructions[11] = 0xe0
return p.PtraceWriteSlice(originAddr, instructions)
}
解决第一个问题,系统调用涉及的寄存器
首先在 golang 源码中分别找到 amd64 和 mips64le 对 syscall.PtraceRegs
的定义,结合 Ptrace 的知识,这里是用户空间中可以访问的寄存器信息。
ztypes_linux_amd64.go
type PtraceRegs struct {
R15 uint64
R14 uint64
R13 uint64
R12 uint64
Rbp uint64
Rbx uint64
R11 uint64
R10 uint64
R9 uint64
R8 uint64
Rax uint64
Rcx uint64
Rdx uint64
Rsi uint64
Rdi uint64
Orig_rax uint64
Rip uint64
Cs uint64
Eflags uint64
Rsp uint64
Ss uint64
Fs_base uint64
Gs_base uint64
Ds uint64
Es uint64
Fs uint64
Gs uint64
}
ztypes_linux_mips64le.go
type PtraceRegs struct {
Regs [102]uint64
U_tsize uint64
U_dsize uint64
U_ssize uint64
Start_code uint64
Start_data uint64
Start_stack uint64
Signal int64
U_ar0 uint64
Magic uint64
U_comm [32]int8
}
结合系统调用的知识,在 amd64 下,系统调用就是将系统调用号放在 rax 寄存器中,然后将参数放在的 Rdi,Rsi,Rdx,R10,R8,R9 中,最后将返回值放在 rax 中。这是系统调用时参数传递的固定寄存器。
那么我们就要知道在 mips64le 下,系统调用是怎么工作的。我们可以在 go 代码中找到系统调用 plan9 的汇编实现。从实现中,我们可以看到,mips64le 系统调用的参数传递使用了 R4-R9 这几个寄存器,同时调用号和返回值使用了 R2。
src/syscall/asm_linux_amd64.s
TEXT ·Syscall6(SB),NOSPLIT,$0-80
CALL runtime·entersyscall(SB)
MOVQ a1+8(FP), DI
MOVQ a2+16(FP), SI
MOVQ a3+24(FP), DX
MOVQ a4+32(FP), R10
MOVQ a5+40(FP), R8
MOVQ a6+48(FP), R9
MOVQ trap+0(FP), AX // syscall entry
SYSCALL
CMPQ AX, $0xfffffffffffff001
JLS ok6
MOVQ $-1, r1+56(FP)
MOVQ $0, r2+64(FP)
NEGQ AX
MOVQ AX, err+72(FP)
CALL runtime·exitsyscall(SB)
RET
ok6:
MOVQ AX, r1+56(FP)
MOVQ DX, r2+64(FP)
MOVQ $0, err+72(FP)
CALL runtime·exitsyscall(SB)
RET
同理,我们可以找到mips64对于 syscall 的实现
src/syscall/asm_linux_mips64.s
TEXT ·Syscall6(SB),NOSPLIT,$0-80
JAL runtime·entersyscall(SB)
MOVV a1+8(FP), R4
MOVV a2+16(FP), R5
MOVV a3+24(FP), R6
MOVV a4+32(FP), R7
MOVV a5+40(FP), R8
MOVV a6+48(FP), R9
MOVV trap+0(FP), R2 // syscall entry
SYSCALL
BEQ R7, ok6
MOVV $-1, R1
MOVV R1, r1+56(FP) // r1
MOVV R0, r2+64(FP) // r2
MOVV R2, err+72(FP) // errno
JAL runtime·exitsyscall(SB)
RET
ok6:
MOVV R2, r1+56(FP) // r1
MOVV R3, r2+64(FP) // r2
MOVV R0, err+72(FP) // errno
JAL runtime·exitsyscall(SB)
RET
此时,我们便找到了在 mips64 下 syscall 使用的几个寄存器了,就是四号到九号寄存器 R4-R9。
第二个问题,解决 PC 寄存器
rip 寄存器即 ip 寄存器,指令指针寄存器,用于记录下一个指令的地址。在 amd64 下,其直接写明了 RIP 寄存器。所以需要找到 mips64le 的指令指针寄存器。
恰好在 src/syscall/syscall_linux_*.go
下,go 为我们提供了各个架构的 PC 寄存器的定义。
root@sjt-pc:/wk/github.com/golang/go# grep -rn "SetPC"
src/syscall/syscall_linux_mipsx.go:212:func (r *PtraceRegs) SetPC(pc uint64) { r.Regs[64] = uint32(pc) }
src/syscall/syscall_linux_386.go:376:func (r *PtraceRegs) SetPC(pc uint64) { r.Eip = int32(pc) }
src/syscall/syscall_linux_amd64.go:141:func (r *PtraceRegs) SetPC(pc uint64) { r.Rip = pc }
src/syscall/syscall_linux_riscv64.go:178:func (r *PtraceRegs) SetPC(pc uint64) { r.Pc = pc }
src/syscall/syscall_linux_mips64x.go:202:func (r *PtraceRegs) SetPC(pc uint64) { r.Regs[64] = pc }
src/syscall/syscall_linux_arm.go:225:func (r *PtraceRegs) SetPC(pc uint64) { r.Uregs[15] = uint32(pc) }
src/syscall/syscall_linux_s390x.go:283:func (r *PtraceRegs) SetPC(pc uint64) { r.Psw.Addr = pc }
src/syscall/syscall_linux_ppc64x.go:110:func (r *PtraceRegs) SetPC(pc uint64) { r.Nip = pc }
src/syscall/syscall_linux_arm64.go:193:func (r *PtraceRegs) SetPC(pc uint64) { r.Pc = pc }
第三个问题,解决系统调用
mips64le 的操作系统的系统调用号定义在 unistd.h
中,找到对于系统调用功能的调用号。 即 4345 和 4346
#define __NR_process_vm_readv (__NR_Linux + 345)
#define __NR_process_vm_writev (__NR_Linux + 346)
第四个问题,解决两个指令的机器码
func (p *TracedProgram) JumpToFakeFunc(originAddr uint64, targetAddr uint64) error {
instructions := make([]byte, 16)
// mov rax, targetAddr;
// jmp rax ;
instructions[0] = 0x48
instructions[1] = 0xb8
binary.LittleEndian.PutUint64(instructions[2:10], targetAddr)
instructions[10] = 0xff
instructions[11] = 0xe0
return p.PtraceWriteSlice(originAddr, instructions)
}
首先理解汇编的含义,即将一个4字节数放入 rax 寄存器,然后跳转到该寄存器值所指向的地址。那么我们需要找到 mips64 下对应的指令,或者说机器码。参考 mips 指令 https://www.cnblogs.com/CoBrAMG/p/9237609.html
我们可以使用 ld 指令加载一个64位数到寄存器中,然后使用 jr 跳转,如下。编译这段汇编代码,并查看其对应的机器码。可以看到 ld 指令被拆分成了好几条指令,因为 mips64 每条指令只有 32 位,所以将这个64位数分四次每次16位分别载入到64位寄存器中。
ld $2, 0x1122334455667788
jr $2
[root@localhost test]# objdump -d main.o
0000000000000000 <test>:
0: 3c021122 lui v0,0x1122
4: 34423344 ori v0,v0,0x3344
8: 00021438 dsll v0,v0,0x10
c: 34425566 ori v0,v0,0x5566
10: 00021438 dsll v0,v0,0x10
14: dc427788 ld v0,30600(v0)
18: 00400008 jr v0
1c: 00000000 nop
按照这个逻辑写出对应的机器码
func (p *TracedProgram) JumpToFakeFunc(originAddr uint64, targetAddr uint64) error {
instructions := make([]byte,28)
var l1,l2,l3,l4 uint16
l4 = uint16(targetAddr & 0xff)
targetAddr = targetAddr >> 16
l3 = uint16(targetAddr & 0xff)
targetAddr = targetAddr >> 16
l2 = uint16(targetAddr & 0xff)
targetAddr = targetAddr >> 16
l1 = uint16(targetAddr & 0xff)
// lui v0,0x1122
instructions[0] = 0x3c
instructions[1] = 0x02
binary.LittleEndian.PutUint16(instructions[2:4],l1)
// ori v0,v0,0x3344
instructions[4] = 0x34
instructions[5] = 0x42
binary.LittleEndian.PutUint16(instructions[6:8],l2)
// dsll v0,v0,0x10
instructions[8] = 0x00
instructions[9] = 0x02
instructions[10] = 0x14
instructions[11] = 0x38
// ori v0,v0,0x5566
instructions[12] = 0x34
instructions[13] = 0x42
binary.LittleEndian.PutUint16(instructions[14:16],l3)
// dsll v0,v0,0x10
instructions[16] = 0x00
instructions[17] = 0x02
instructions[18] = 0x14
instructions[19] = 0x38
// ld,v0,0x7788
instructions[20] = 0xdc
instructions[21] = 0x42
binary.LittleEndian.PutUint16(instructions[22:24],l4)
// jr v0
instructions[24] = 0x00
instructions[25] = 0x40
instructions[26] = 0x00
instructions[27] = 0x08
return p.PtraceWriteSlice(originAddr, instructions)
}