背景
这篇文章起因是周末在Linux上写的个小程序发生了栈溢出,然而我的简易crash handler没有捕获到,于是简单记录一下。
这个问题其实很简单:在crash sdk初始化的时候调用sigaltstack
来准备一个备用栈,在栈溢出的时候用于运行signal handler(设置了 SA_ONSTACK flag),然而有个显然的问题我没注意:sigaltstack 是为当前线程设置备用栈的,如果只是在初始化crash sdk的线程中调用 sigaltstack,那么其他线程栈溢出的时候自然是抓不到的。
不过这个问题在Android上还有点不同:
- 上面的“策略”(只在初始化crash sdk的线程中调用 sigaltstack)在Android上是work的,其他线程的栈溢出也能捕获,这是为啥呢?
- 在Android上,我们的 sigaction 不设置
SA_ONSTACK
,“大概率”也能捕获栈溢出的问题,这又是为啥呢?
sigaltstack 的实现 & SA_ONSTACK 的作用
sigaltstack
sigaltstack 在内核中的实现是:do_sigaltstack
static int
do_sigaltstack (const stack_t *ss, stack_t *oss, unsigned long sp)
{
struct task_struct *t = current;
if (oss) {
memset(oss, 0, sizeof(stack_t));
oss->ss_sp = (void __user *) t->sas_ss_sp;
oss->ss_size = t->sas_ss_size;
oss->ss_flags = sas_ss_flags(sp) |
(current->sas_ss_flags & SS_FLAG_BITS);
}
if (ss) {
void __user *ss_sp = ss->ss_sp;
size_t ss_size = ss->ss_size;
unsigned ss_flags = ss->ss_flags;
int ss_mode;
if (unlikely(on_sig_stack(sp)))
return -EPERM;
ss_mode = ss_flags & ~SS_FLAG_BITS;
if (unlikely(ss_mode != SS_DISABLE && ss_mode != SS_ONSTACK &&
ss_mode != 0))
return -EINVAL;
if (ss_mode == SS_DISABLE) {
ss_size = 0;
ss_sp = NULL;
} else {
if (unlikely(ss_size < MINSIGSTKSZ))
return -ENOMEM;
}
t->sas_ss_sp = (unsigned long) ss_sp;
t->sas_ss_size = ss_size;
t->sas_ss_flags = ss_flags;
}
return 0;
}
- 注意其中的
current
,他是用于获取指向当前线程结构的指针,这段逻辑比较简单:就是将我们提供的备用栈的 ss_sp & ss_size 分别设置到当前线程结构task_struct
的 sas_ss_sp & sas_ss_size 字段中 - 从这段代码可以看出:sigaltstack 只会为当前线程设置备用栈,因此若想支持捕获所有线程的栈溢出问题应该在所有线程上调用 sigaltstack
SA_ONSTACK
SA_ONSTACK 这个 flag 主要是内核在handle signal 时,会根据这个 flag 来获取 signal handler 运行时的初始sp,具体可以看:sigsp
static inline unsigned long sigsp(unsigned long sp, struct ksignal *ksig)
{
if (unlikely((ksig->ka.sa.sa_flags & SA_ONSTACK)) && ! sas_ss_flags(sp))
#ifdef CONFIG_STACK_GROWSUP
return current->sas_ss_sp;
#else
return current->sas_ss_sp + current->sas_ss_size;
#endif
return sp;
}
static inline int sas_ss_flags(unsigned long sp)
{
if (!current->sas_ss_size)
return SS_DISABLE;
return on_sig_stack(sp) ? SS_ONSTACK : 0;
}
从这段代码可以看出:只有设置了 SA_ONSTACK
flag 并且当前线程已经设置备用栈时,signal handler 才会在我们设置的备用栈上运行
Android 的特殊之处
不设置备用栈、栈溢出时 signal handler 也能正常执行
我们可以在 signal handler 中加段代码来判断这个时候 handler 的 stack frame 其实不在线程栈中:
static void sighandler(int, struct siginfo*, void*) {
pthread_attr_t attr;
if (pthread_getattr_np(pthread_self(), &attr) != 0) {
FATAL("failed to get pthread's attr");
}
void* stackBase;
size_t stackSize;
if (pthread_attr_getstack(&attr, &stackBase, &stackSize) != 0) {
FATAL("failed to get stack");
}
stack_t sigStack;
if (sigaltstack(nullptr, &sigStack) != 0) {
FATAL("failed to get old sig stack");
}
LOGI("stack base: %p, stack size: %ld, sig stack base: %p, sig stack size: %ld, curr stack value addr: %p"
, stackBase, stackSize, sigStack.ss_sp, sigStack.ss_size, &stackSize);
// ...
}
- 通过
pthread_attr_getstack
可以拿到线程栈:stack base & stack size - 通过
sigaltstack(nullptr, &sigStack)
可以在不修改备用栈的情况下去获取当前线程的备用栈信息 &stackSize
当前函数局部变量是分配在当前 stack frame 中的,通过它的地址可以看出 stack frame 的位置
在 Linux 上,如果没有为线程设置备用栈,那么取出的 stack base & stack size 都是 0,从上面的测试代码中可以看出在 Android 上虽然我们没有主动为线程设置备用栈,但是是能取到的,而且通过局部变量的地址可以看出当前的 signal handler 也确实运行在这个备用栈上的。
Android 上线程的备用栈从何而来?
Android上的线程似乎默认有一个备用栈,那这个备用栈是何时设置的呢?可以通过 hook + 抓栈 的方式来定位:
- PLT hook sigaltstack
- aarch64 上基于fp做栈回溯
- 对pc符号化,拿到对应的库 & 符号名(这里通过dladdr不行,因为调用的符号不是导出的动态符号,可以用xDL)
static void unwind(uintptr_t fp, int max) {
auto stackInfo = getCurrThreadStack();
if (!stackInfo) {
return;
}
int count = 0;
static void* cache = nullptr;
while (count < max && isFpValid(fp, stackInfo->stackBase, stackInfo->stackTop)) {
auto fpp = reinterpret_cast<uintptr_t*>(fp);
auto preFp = *fpp;
auto preLr = *(fpp + 1);
fp = preFp;
xdl_info_t info;
if (xdl_addr((void*)(preLr - 4), &info, &cache)) {
LOGI("%s:%s", info.dli_fname, info.dli_sname);
}
}
}
static int sigaltstackProxy(const stack_t* newStack, const stack_t* oldStack) {
BYTEHOOK_STACK_SCOPE();
unwind((uintptr_t)__builtin_frame_address(0), 64);
return BYTEHOOK_CALL_PREV(sigaltstackProxy, newStack, oldStack);
}
通过上面的demo代码可以定位到:在Android上创建出线程后会先执行__pthread_start
在其中会为当前线程设置备用栈,之后才会去执行start_routine
,可以参考 bionic 中的 pthread_create.cpp。(其实这个不用hook也容易猜到~)
Android 上不设置 SA_ONSTACK 为啥也能在备用栈上运行?
从 Linux 内核代码看,如果我们的 sigaction 没有添加SA_ONSTACK
flag 的话,即使我们为线程设置了备用栈,也不会使用的:
static inline unsigned long sigsp(unsigned long sp, struct ksignal *ksig)
{
if (unlikely((ksig->ka.sa.sa_flags & SA_ONSTACK)) && ! sas_ss_flags(sp))
#ifdef CONFIG_STACK_GROWSUP
return current->sas_ss_sp;
#else
return current->sas_ss_sp + current->sas_ss_size;
#endif
return sp;
}
然而,在 Android 上在不添加 SA_ONSTACK
flag 的情况下,依然能在备用栈上运行,并且能捕获栈溢出的case,这又是为啥呢?
通过抓栈也能发现我们的handler是由_ZN3art11SignalChain7HandlerEiP7siginfoPv
(也就是:art::SignalChain::Handler(int, siginfo*, void*)
)调过来的。
因为Android中java代码可能会被jit生成机器码,生成的机器码中的 NPE 表现为 SIGSEGV,art 虚拟机需要识别出这种case,于是 art 搞了个 sigchain,在其中注册了一个signal handler 来处理这种情况,我们注册的 signal handler 会被放到 chain 中,由他代理调用(如果不是java的npe的话),所以以SIGSEGV为例,向系统注册signal handler的是 libsigchain,而他注册的时候 flags 中是有 SA_ONSTACK,我们加不加其实不影响:
void Register(int signo) {
#if defined(__BIONIC__)
struct sigaction64 handler_action = {};
sigfillset64(&handler_action.sa_mask);
#else
struct sigaction handler_action = {};
sigfillset(&handler_action.sa_mask);
#endif
handler_action.sa_sigaction = SignalChain::Handler;
handler_action.sa_flags = SA_RESTART | SA_SIGINFO | SA_ONSTACK;
#if defined(__BIONIC__)
linked_sigaction64(signo, &handler_action, &action_);
#else
linked_sigaction(signo, &handler_action, &action_);
#endif
}
不过有一点要注意的是:栈溢出也有可能触发的是 SIGBUS,而 art sigchain 没有为 SIGBUS 注册handler,所以这个case下如果我们的 sigaction flags 中没有 SA_ONSTACK,那么便不会使用备用栈了。
总结
Linux
- glibc 线程默认没有备用栈,如果要捕获栈溢出的问题,需要自己设置
- signal handler要使用备用栈,必须添加 SA_ONSTACK(如果没有设置备用栈但添加了这个flag,会使用当前线程栈,对于栈溢出的case,会触发 force_sigsegv)
void force_sigsegv(int sig, struct task_struct *p)
{
if (sig == SIGSEGV) {
unsigned long flags;
spin_lock_irqsave(&p->sighand->siglock, flags);
p->sighand->action[sig - 1].sa.sa_handler = SIG_DFL;
spin_unlock_irqrestore(&p->sighand->siglock, flags);
}
force_sig(SIGSEGV, p);
}
Android
-
bionic 线程在运行用户的 start_routine 之前会为线程创建备用栈(mmap),在线程退出时回收(munmap)
- 所以不用我们创建备用栈了,而且如果我们创建了的话,bionic创建的那个也得等线程退出时才会回收,会多占虚存
- 如果bionic没默认设置备用栈的话,我们捕获栈溢出的问题还比较麻烦:对于crash sdk初始化后创建的线程我们可以通过hook pthread_create、pthread_exit 来处理,但是app启动的时候系统已经创建了一些线程,他们的栈溢出怎么捕获呢
-
不设置 SA_ONSTACK 依然能在备用栈上运行:是因为对于 SIGSEGV,我们的 sigaction 设置被 libsigchain 给代理了,真正设置这个 sigaction 的是 libsigchain,而他是带了 SA_ONSTACK flag 的。所以实际上要想在备用栈上运行 SA_ONSTACK 还是必须的