Linux kernel signal原理(下)- aarch64架构sigreturn流程

一、前言

在上篇中写到了linux中signal的处理流程,在do_signal信号处理的流程最后,会通过sigreturn再次回到线程现场,上篇文章中介绍了在X86_64架构下的实现,本篇中介绍下在aarch64架构下的实现原理。

二、sigaction系统调用

objectivec 复制代码
#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系统调用做的一个简单的测试。

bash 复制代码
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下的内存映射,如下所示:

bash 复制代码
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]

其中:

bash 复制代码
7f94ded000-7f94dee000 r-xp 00000000 00:00 0                              [vdso]

可以看到vdso内存大小:0x1000 = 4096,即一个page的大小。

vdso的起始虚拟地址在进程974770是: 7f94ded000,转化为十进制即547958476800,将这段内存dump到文件中:

bash 复制代码
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镜像,可以对其进行符号查找:

bash 复制代码
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的实现在构建用户态栈帧的时候可以看到如下关键流程:

objectivec 复制代码
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;
}
objectivec 复制代码
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函数的上下文,并继续执行。

相关推荐
卡戎-caryon16 分钟前
【Linux网络与网络编程】07.应用层协议HTTPS
linux·网络·网络协议·tcp/ip·https·应用层协议
Hi-Dison26 分钟前
Ubuntu与OpenHarmony OS 5.0显示系统架构比较
linux·ubuntu·系统架构
诺亚凹凸曼28 分钟前
C++ linux打包运行方案(cmake)
linux·开发语言·c++
梁萌1 小时前
10-DevOps-Jenkins参数化构建实现多版本发布
运维·gitlab·jenkins·devops·tag
雨月琉琉2 小时前
备份jenkins
运维·servlet·jenkins
新青年5792 小时前
CentOS的安装以及网络配置
linux·网络·centos
tingting01192 小时前
jenkins pipeline ssh协议报错处理
服务器·ssh·jenkins
一只帆記2 小时前
Jenkins 简易使用记录
运维·jenkins
Guheyunyi2 小时前
安全调度系统:安全管理的智能中枢
运维·安全·信息可视化·数据挖掘·数据分析
网络研究院2 小时前
安全文件共享实际上是什么样的呢?
运维·网络·安全