其实只要接触过 Linux 内核源码的人都应该见过 current
这个宏,使用它获取当前进程的 task_struct
结构(当然这个不是绝对的)
现在就来看看 current
真正的样子
1 | /* SPDX-License-Identifier: GPL-2.0 */ |
DECLARE_PER_CPU(struct task_struct *, current_task);
表示在 .data..percpu
数据段声明一个 名为:current_task
的 struct task_struct *
可以看到 current
是个宏,真正调用的是 get_current 函数 可以看到返回值是一个 task_struct *,总是 内联 ,也就是说正常编译出来不会有函数体
1 | static __always_inline struct task_struct *get_current(void) |
然后 get_current 函数调用 this_cpu_read_stable
1 |
在 x86-64 体系下,所以 sizeof(current_task)
的值为 8
对应的是:
1 | asm(op "q "__percpu_arg(P1)",%0" \ |
1 |
linux 最喜欢的 “万层” define 嵌套
1 |
这里就是终点了
在 X86_64 体系下面是 gs
; mov 操作指令是 movq
否则的话 就是 fs
; mov 操作指令是 movl
现在看来就是一直在拼接字符,宏展开就是一句汇编,完整的代码是:
1 | asm(movq "%%gs:%P1","%0" : "=r" (var) :"p" (&(var)) |
编译完后得到的会是像这样的
1 | mov rdi, QWORD PTR gs:0xXXXXXXXX |
写个 demo 去动态调试,我选的是 getpid
code:
1 |
|
在内核里 getpid 会在调用 __task_pid_nr_ns 前,获取 current task 的 task_struct 的地址作为参数(第一个,放入 rdi)
源码:
1 | SYSCALL_DEFINE0(getpid) |
静态编译,打包成 initrd
1 | ➜ cat init.c |
调试
1 | qemu-system-x86_64 -kernel arch/x86_64/boot/bzImage -append "nokaslr" -initrd init.img -S -s |
直接在 do_syscall_64 下硬件断点,这是 x86-64 系统调用进入内核的必经之路
getpid 的系统调用号是,39
1 | ➜ ~ cat /usr/include/asm/unistd_64.h| grep getpid |
就一直 c 到 do_syscall_64 的参数 nr 为 39(0x27)停下
看到:do_syscall_64 (nr=0x27, regs=0xffffc90000013f58)
在 task_tgid_vnr
下断点(其实不用在系统调用那里下断点,直接在 task_tgid_vnr 下断点也是一样可以断下来的)然后 c
可以看到上面那两句汇编
1 | 0xffffffff81079f97 <__x64_sys_getpid+7> mov rdi, QWORD PTR gs:0x17d00 |
从 gs:0x17d00
处取一个 QWORD 大小的数据,放入 rdi,再调用 __task_pid_nr_ns
,根据 x86-64 的函数调用约定,rdi 存的就是调用函数的第一个参数
__task_pid_nr_ns 的源码上面已经贴出来过了,函数原型
1 | pid_t __task_pid_nr_ns(struct task_struct *task, enum pid_type type, |
传入的就是 current 的 task_struct 的地址(仔细看上面给出的代码)
1 | gef➤ p $gs |
gs 寄存器的值是 0,看看 vmlinux, VMA 为 0 是哪个 section 的地址起始地址,其实就是 .data..percpu
1 | ➜ linux-5.6 objdump -h vmlinux |
可以.data..percpu
的 VMA
为 0
大小为 0x2b158
其实当前进程的 task_struct
的地址会存在 .data..percpu
,名为 current_task
1 | DECLARE_PER_CPU(struct task_struct *, current_task); |
详细看:http://linux.laoqinren.net/kernel/percpu-var/
其实就能知道了 gs
就是指向的 .data..percpu
的起始地址(虚拟地址 VMA),而.data..percpu
的起始地址(虚拟地址 VMA)为 0,current_task
在 .data..percpu
的偏移量为 0x17d00
看内核导出的符号
1 | ➜ linux-5.6 nm vmlinux | grep current_task |
D
表示 该符号是个全局变量,放在某个数据段中
man: https://www.man7.org/linux/man-pages/man1/nm.1p.html
1 | gef➤ p ¤t_task |
看到 current_task 的地址就是 0x17d00
,解引用就能得到当前进程的 task_struct
的地址(当然 gdb 不能访问这个地址的内容)
完!