首先准备以下go程序:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21package main
import (
"math/rand"
"time"
"fmt"
)
func add(a, b int32)int32{
return a + b
}
func main() {
for{
a := rand.Int31n(100)
b := rand.Int31n(100)
r := add(a, b)
fmt.Printf("%d + %d = %d\n", a, b, r)
time.Sleep(time.Second)
}
}
这里我们主要声明一个add方法,并在main函数中调用它,之后编译并运行该程序1
$ go build -gcflags='-N -l' main.go && ./main
开启另一个shell,我们要来获取add函数在代码段中的地址:1
2
3
4$ objdump --syms main | grep add
...
0000000000480220 g F .text 0000000000000034 main.add
...1
2$ cat /proc/`pgrep main`/maps | grep /root/study/main | grep r-xp
00400000-00481000 r-xp 00000000 fd:00 102732081 /root/study/main
通过/proc/{pid}/maps查看程序代码段的虚拟内存起始地址为0x400000,这里需要减去相应的偏移量0x480220 - 0x400000 = 0x80220,得到add方法地址,接下来将地址通过debugfs写入uprobe
我们如何写入uprobe调式信息呢,uprobe的事件格式如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22p[:[GRP/]EVENT] PATH:OFFSET [FETCHARGS] : Set a uprobe
r[:[GRP/]EVENT] PATH:OFFSET [FETCHARGS] : Set a return uprobe (uretprobe)
-:[GRP/]EVENT : Clear uprobe or uretprobe event
GRP : Group name. If omitted, "uprobes" is the default value.
EVENT : Event name. If omitted, the event name is generated based
on PATH+OFFSET.
PATH : Path to an executable or a library.
OFFSET : Offset where the probe is inserted.
FETCHARGS : Arguments. Each probe can have up to 128 args.
%REG : Fetch register REG
@ADDR : Fetch memory at ADDR (ADDR should be in userspace)
@+OFFSET : Fetch memory at OFFSET (OFFSET from same file as PATH)
$stackN : Fetch Nth entry of stack (N >= 0)
$stack : Fetch stack address.
$retval : Fetch return value.(*)
+|-offs(FETCHARG) : Fetch memory at FETCHARG +|- offs address.(**)
NAME=FETCHARG : Set NAME as the argument name of FETCHARG.
FETCHARG:TYPE : Set TYPE as the type of FETCHARG. Currently, basic types
(u8/u16/u32/u64/s8/s16/s32/s64), "string" and bitfield
are supported.
我们只要将uprobe事件写入/sys/kernel/debug/tracing/uprobe_events文件就可以了:1
2$ echo 'p:entry_add /root/study/main:0x80220 %ax:u32 %bx:u32' > /sys/kernel/debug/tracing/uprobe_events
$ echo 'r:exit_add /root/study/main:0x80220 %ax:u32' >> /sys/kernel/debug/tracing/uprobe_events
这里eax和ebx两个寄存器保存了函数的入参,而函数返回时把返回值保存在了eax寄存器,这个后面我们可以从汇编代码看到。
接下来先清理掉之前的事件日志:1
$ echo > /sys/kernel/debug/tracing/trace
打开debug开关:1
$ echo 1 > /sys/kernel/debug/tracing/events/uprobes/enable
查看事件日志:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32$ cat /sys/kernel/debug/tracing/trace
# tracer: nop
#
# _-----=> irqs-off
# / _----=> need-resched
# | / _---=> hardirq/softirq
# || / _--=> preempt-depth
# ||| / delay
# TASK-PID CPU# |||| TIMESTAMP FUNCTION
# | | | |||| | |
main-25197 [001] d... 853041.215718: entry_add: (0x480220) arg1=81 arg2=87
| | | | |
进程ID 事件名 函数入口地址 参数a 参数b
main-25197 [001] d... 853041.215746: exit_add: (0x4802b6 <- 0x480220) arg1=168
| | | | |
进程ID 事件名 函数入口地址 函数返回地址 函数返回值
main-25197 [001] d... 853042.216402: entry_add: (0x480220) arg1=47 arg2=59
main-25197 [001] d... 853042.216415: exit_add: (0x4802b6 <- 0x480220) arg1=106
main-25197 [000] d... 853043.219246: entry_add: (0x480220) arg1=81 arg2=18
main-25197 [000] d... 853043.219254: exit_add: (0x4802b6 <- 0x480220) arg1=99
main-25197 [000] d... 853044.221530: entry_add: (0x480220) arg1=25 arg2=40
main-25197 [000] d... 853044.221539: exit_add: (0x4802b6 <- 0x480220) arg1=65
main-25197 [000] d... 853045.221832: entry_add: (0x480220) arg1=56 arg2=0
main-25197 [000] d... 853045.221842: exit_add: (0x4802b6 <- 0x480220) arg1=56
main-25197 [000] d... 853050.230817: entry_add: (0x480220) arg1=37 arg2=6
main-25197 [000] d... 853050.230827: exit_add: (0x4802b6 <- 0x480220) arg1=43
main-25197 [000] d... 853052.234467: entry_add: (0x480220) arg1=28 arg2=58
main-25197 [000] d... 853052.234476: exit_add: (0x4802b6 <- 0x480220) arg1=86
main-25197 [000] d... 853053.234655: entry_add: (0x480220) arg1=47 arg2=47
main-25197 [000] d... 853053.234664: exit_add: (0x4802b6 <- 0x480220) arg1=94
main-25197 [000] d... 853054.237878: entry_add: (0x480220) arg1=87 arg2=88
main-25197 [000] d... 853054.237887: exit_add: (0x4802b6 <- 0x480220) arg1=175
对比go程序的输出:1
2
3
4
5
6
7
8
9
10
11
12
13
1481 + 87 = 168
47 + 59 = 106
81 + 18 = 99
25 + 40 = 65
56 + 0 = 56
94 + 11 = 105
62 + 89 = 151
28 + 74 = 102
11 + 45 = 56
37 + 6 = 43
95 + 66 = 161
28 + 58 = 86
47 + 47 = 94
87 + 88 = 175
可以看出来事件日志是能正确抓取每次调用及其参数和返回值的。接下来,我们通过gdb attach到该进程,可以看到main.add的入口处的指令被uprobe改成了int3指令了,这里我们可以注意到函数的入口地址为0x480220,确实和日志里面打印的entry_add: (0x480220)一致:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20gdb -p `pgrep main`
(gdb) info line main.add
....
(gdb) disas main.add
...
Dump of assembler code for function main.add:
0x0000000000480220 <+0>: int3
0x0000000000480221 <+1>: sub $0x10,%esp
0x0000000000480224 <+4>: mov %rbp,0x8(%rsp)
0x0000000000480229 <+9>: lea 0x8(%rsp),%rbp
0x000000000048022e <+14>: mov %eax,0x18(%rsp)
0x0000000000480232 <+18>: mov %ebx,0x1c(%rsp)
0x0000000000480236 <+22>: movl $0x0,0x4(%rsp)
0x000000000048023e <+30>: mov 0x18(%rsp),%eax
0x0000000000480242 <+34>: add 0x1c(%rsp),%eax
0x0000000000480246 <+38>: mov %eax,0x4(%rsp)
0x000000000048024a <+42>: mov 0x8(%rsp),%rbp
0x000000000048024f <+47>: add $0x10,%rsp
0x0000000000480253 <+51>: ret
End of assembler dump.
但在这里我们确找不到exit_add: (0x4802b6 <- 0x480220) 0x480220这个地址,由于add函数是由main函数调用的,因此我们猜想0x480220这个地址是不是在main函数中,于是我们再查看一下main函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16(gdb) disas main.main
Dump of assembler code for function main.main:
.....
0x000000000048028f <+47>: call 0x460860 <math/rand.Int31n>
0x0000000000480294 <+52>: mov %eax,0x34(%rsp)
0x0000000000480298 <+56>: mov $0x64,%eax
0x000000000048029d <+61>: nopl (%rax)
0x00000000004802a0 <+64>: call 0x460860 <math/rand.Int31n>
0x00000000004802a5 <+69>: mov %eax,0x30(%rsp)
0x00000000004802a9 <+73>: mov 0x34(%rsp),%ecx
0x00000000004802ad <+77>: mov %eax,%ebx
0x00000000004802af <+79>: mov %ecx,%eax
0x00000000004802b1 <+81>: call 0x480220 <main.add>
0x00000000004802b6 <+86>: mov %eax,0x2c(%rsp)
....
End of assembler dump.
确实我们也可以看到整个add函数调用的全过程,两个随机数被放入eax和ebx两个寄存器,函数返回值被放入栈中0x2c(%rsp),而该指令正是 0x480220。
接下来我们关掉uprobe调试:1
$ echo 0 > /sys/kernel/debug/tracing/events/uprobes/enable
接着我们再回去看add函数的入口处指令:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15(gdb) disas main.add
Dump of assembler code for function main.add:
0x0000000000480220 <+0>: sub $0x10,%rsp
0x0000000000480224 <+4>: mov %rbp,0x8(%rsp)
0x0000000000480229 <+9>: lea 0x8(%rsp),%rbp
0x000000000048022e <+14>: mov %eax,0x18(%rsp)
0x0000000000480232 <+18>: mov %ebx,0x1c(%rsp)
0x0000000000480236 <+22>: movl $0x0,0x4(%rsp)
0x000000000048023e <+30>: mov 0x18(%rsp),%eax
0x0000000000480242 <+34>: add 0x1c(%rsp),%eax
0x0000000000480246 <+38>: mov %eax,0x4(%rsp)
0x000000000048024a <+42>: mov 0x8(%rsp),%rbp
0x000000000048024f <+47>: add $0x10,%rsp
0x0000000000480253 <+51>: ret
End of assembler dump.
此时发现该指令(0x480220)已被恢复成了正常的指令了。