一、前言
? ? ? ?在上篇中寫到了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-littleaarch64DYNAMIC 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; //X1regs->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;elsesigtramp = 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函數的上下文,并繼續執行。