一、前言
在上篇中写到了linux中signal的处理流程,在do_signal信号处理的流程最后,会通过sigreturn再次回到线程现场,上篇文章中介绍了在X86_64架构下的实现,本篇中介绍下在aarch64架构下的实现原理。
二、sigaction系统调用
#include <signal.h>
#include <stdio.h>
#include <string.h>
void signal_handler(int signum, siginfo_t *siginfo, void *context)
{
printf("Received signal %d\n", signum);
printf("Send by PID: %d\n", siginfo->si_pid);
}
int main()
{
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_sigaction = signal_handler;
act.sa_flags = SA_SIGINFO;
if (sigaction(SIGTERM, &act, NULL) < 0) {
perror("sigaction");
return 1;
}
while (1) {
printf("perfect\n");
sleep(10);
}
return 0;
}
如上是使用sigaction系统调用做的一个简单的测试。
1、放到环境上,并使用strace跟踪进程的系统调用。strace ./test_siginfo
2、向该进程发送SIGTERM信号
可以看到用户态进程在处理SIGTERM信号之后,通过特殊的rt_sigreturn系统调用到内核之后,又再次返回到用户态执行。具体这个rt_sigreturn从哪里来的,下面分析下。
首先看了下glibc的源码,看下在注册sigaction函数的时候是否会把sigreturn系统调用也注册进去,通过阅读源码发现x86_64是采用这个方式实现的,但是aarch64不是。
有事就找man,通过看了下man 2 sigreturn,看到了如下关键信息:
怀疑aarch64 架构下是通过vdso实现的。
三、sigreturn实现流程
通过查看上述测试进程在/proc下的内存映射,如下所示:
sh-5.0# cat /proc/974770/maps
00400000-00401000 r-xp 00000000 b3:07 255 /data/test_siginfo
00410000-00411000 r--p 00000000 b3:07 255 /data/test_siginfo
00411000-00412000 rw-p 00001000 b3:07 255 /data/test_siginfo
06801000-06822000 rw-p 00000000 00:00 0 [heap]
7f94c28000-7f94d81000 r-xp 00000000 b3:01 424154 /usr/lib/aarch64-linux-gnu/libc-2.31.so
7f94d81000-7f94d90000 ---p 00159000 b3:01 424154 /usr/lib/aarch64-linux-gnu/libc-2.31.so
7f94d90000-7f94d93000 r--p 00158000 b3:01 424154 /usr/lib/aarch64-linux-gnu/libc-2.31.so
7f94d93000-7f94d96000 rw-p 0015b000 b3:01 424154 /usr/lib/aarch64-linux-gnu/libc-2.31.so
7f94d96000-7f94d99000 rw-p 00000000 00:00 0
7f94da7000-7f94dac000 r-xp 00000000 b3:01 42742 /usr/lib64/libpsh_essence.so
7f94dac000-7f94dbb000 ---p 00005000 b3:01 42742 /usr/lib64/libpsh_essence.so
7f94dbb000-7f94dbc000 r--p 00004000 b3:01 42742 /usr/lib64/libpsh_essence.so
7f94dbc000-7f94dbd000 rw-p 00005000 b3:01 42742 /usr/lib64/libpsh_essence.so
7f94dbd000-7f94dde000 r-xp 00000000 b3:01 423760 /usr/lib/aarch64-linux-gnu/ld-2.31.so
7f94de6000-7f94dea000 rw-p 00000000 00:00 0
7f94deb000-7f94ded000 r--p 00000000 00:00 0 [vvar]
7f94ded000-7f94dee000 r-xp 00000000 00:00 0 [vdso]
7f94dee000-7f94def000 r--p 00021000 b3:01 423760 /usr/lib/aarch64-linux-gnu/ld-2.31.so
7f94def000-7f94df1000 rw-p 00022000 b3:01 423760 /usr/lib/aarch64-linux-gnu/ld-2.31.so
7feceb1000-7feced2000 rw-p 00000000 00:00 0 [stack]
其中:
7f94ded000-7f94dee000 r-xp 00000000 00:00 0 [vdso]
可以看到vdso内存大小:0x1000 = 4096,即一个page的大小。
vdso的起始虚拟地址在进程974770是: 7f94ded000,转化为十进制即547958476800,将这段内存dump到文件中:
sh-5.0# dd if=/proc/974770/mem of=/tmp/linus-vdso.so skip=547958476800 ibs=1 count=4096
dd: /proc/974770/mem: cannot skip to specified offset
4096+0 records in
8+0 records out
4096 bytes (4.1 kB, 4.0 KiB) copied, 0.0178463 s, 230 kB/s
由于vdso是一个完整的ELF镜像,可以对其进行符号查找:
sh-5.0# objdump -T /tmp/linus-vdso.so
/tmp/linus-vdso.so: file format elf64-littleaarch64
DYNAMIC SYMBOL TABLE:
0000000000000000 g DO ABS 0000000000000000 LINUX_2.6.39 LINUX_2.6.39
0000000000000750 g DF .text 0000000000000078 LINUX_2.6.39 __kernel_clock_getres
00000000000007cc g D .text 0000000000000008 LINUX_2.6.39 __kernel_rt_sigreturn
00000000000005a0 g DF .text 00000000000001b0 LINUX_2.6.39 __kernel_gettimeofday
0000000000000300 g DF .text 00000000000002a0 LINUX_2.6.39 __kernel_clock_gettime
从符号表中可以看出,确实是有__kernel_rt_sigreturn的实现。
下面看下内核是如何实现的:
通过阅读内核源码,handle_signal的实现在构建用户态栈帧的时候可以看到如下关键流程:
static int setup_rt_frame(int usig, struct ksignal *ksig, sigset_t *set,
struct pt_regs *regs)
{
struct rt_sigframe_user_layout user;
struct rt_sigframe __user *frame;
int err = 0;
fpsimd_signal_preserve_current_state();
if (get_sigframe(&user, ksig, regs))
return 1;
frame = user.sigframe;
__put_user_error(0, &frame->uc.uc_flags, err);
__put_user_error(NULL, &frame->uc.uc_link, err);
err |= __save_altstack(&frame->uc.uc_stack, regs->sp);
err |= setup_sigframe(&user, regs, set);
if (err == 0) {
setup_return(regs, &ksig->ka, &user, usig); //信号返回关键函数
if (ksig->ka.sa.sa_flags & SA_SIGINFO) { //如果注册的时候传入了SA_SIGINFO标记,就会把X1,X2寄存器值传给用户态回调
err |= copy_siginfo_to_user(&frame->info, &ksig->info);
regs->regs[1] = (unsigned long)&frame->info; //X1
regs->regs[2] = (unsigned long)&frame->uc; //X2
}
}
return err;
}
static void setup_return(struct pt_regs *regs, struct k_sigaction *ka,
struct rt_sigframe_user_layout *user, int usig)
{
__sigrestore_t sigtramp;
regs->regs[0] = usig;
regs->sp = (unsigned long)user->sigframe;
regs->regs[29] = (unsigned long)&user->next_frame->fp;
regs->pc = (unsigned long)ka->sa.sa_handler;
/*
* Signal delivery is a (wacky) indirect function call in
* userspace, so simulate the same setting of BTYPE as a BLR
* <register containing the signal handler entry point>.
* Signal delivery to a location in a PROT_BTI guarded page
* that is not a function entry point will now trigger a
* SIGILL in userspace.
*
* If the signal handler entry point is not in a PROT_BTI
* guarded page, this is harmless.
*/
if (system_supports_bti()) {
regs->pstate &= ~PSR_BTYPE_MASK;
regs->pstate |= PSR_BTYPE_C;
}
/* TCO (Tag Check Override) always cleared for signal handlers */
regs->pstate &= ~PSR_TCO_BIT;
if (ka->sa.sa_flags & SA_RESTORER) //x86_64架构默认实现
sigtramp = ka->sa.sa_restorer;
else
sigtramp = VDSO_SYMBOL(current->mm->context.vdso, sigtramp); //aarch_64架构实现方式
regs->regs[30] = (unsigned long)sigtramp; //将sigreturn系统调用地址保存在X30寄存器中
}
通过以上代码可以很清晰的看出在aarch64架构下的实现,即首先会在VDSO的符号表中找到sigreturn的地址,然后保存在X30寄存器中,X30寄存器保存的是函数的返回地址,即在用户态handler执行完成之后要执行的函数地址。对arm寄存器不熟悉的可以参考之前的文章:
ARM64架构栈帧以及帧指针FP-CSDN博客
整个流程可以归结如下图所示:
1、用户程序注册了处理函数signal_handler来捕获SIGTERM信号。
2、当前正在执行main函数时,若发生中断或异常导致切换到内核态。
3、在中断处理完成后,在返回用户态执行main函数之前,检测到有SIGTERM信号pending。
4、内核决定在返回用户态后,不恢复main函数的上下文继续执行,而是调用signal_handler函数。signal_handler函数和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
5、signal_handler函数执行完毕后,会自动执行特殊的系统调用sigreturn,再次进入内核态。
6、如果没有新的信号pending,此次返回用户态将会恢复main函数的上下文,并继续执行。