如何获取跨系统调用的函数调用栈

在进行功能调试或者问题定位时,经常需要找一下哪里触发的系统调用,并跟踪一下系统调用过程。

一种方法是使用simpleperf

css 复制代码
simpleperf record -g -o  -- 
python3 /android-ndk-r27c/simpleperf/gecko_profile_generator.py \
      -i  \
      --symfs  \
      --kallsyms   | gzip > 

浏览器打开https://profiler.firefox.com/将生成的add_client.perf.json.gz拖进去,就可以查看调用树、火焰图、栈图等进一步分析函数调用关系。

参考另一篇文章 尝试通过一个demo分析binder的执行流程 的使用simpleperf抓取通讯双方的函数调用栈小节

或者使用基于eBPF的工具比如stackplz 参考 :

这两种方式在尝试定位某些问题时比较受限,比如kernel启动早期时。因此本文尝试一种更加直接的方式:直接在目标位置打印函数调用栈。

获取用户态和内核态的函数调用栈

首先确保 CONFIG_STACKTRACE CONFIG_KALLSYMSCONFIG_USER_STACKTRACE_SUPPORT内核宏打开

可以通过 zcat /proc/config.gz | grep -E "STACKTRACE|KALLSYMS" 确认。

在代码中插入以下代码:

c 复制代码
#if IS_ENABLED(CONFIG_STACKTRACE)
#include 
#define STACK_ENTRIES 64

/*
 * 打印当前进程的
 * 1) 内核调用栈(符号名)
 * 2) 用户空间调用栈(原始 PC 地址)
 */
static void dump_all_stacks(void)
{
    int i;
    unsigned int nr;
    unsigned long entries[STACK_ENTRIES];

    /* 基本进程信息 */
    pr_info("pid=%d comm=%s\n",
            current->pid, current->comm);

    memset(entries, 0, sizeof(entries));
    nr = stack_trace_save(entries, ARRAY_SIZE(entries), 0);

    pr_info("kernel backtrace:\n");
    for (i = 0; i < nr; i++) {
        /* %pS 依赖 CONFIG_KALLSYMS,可以打印出符号名 */
        pr_info(&#34;  [k%02d] %pS\n&#34;, i, (void *)entries[i]);
    }

#if IS_ENABLED(CONFIG_USER_STACKTRACE_SUPPORT)
    /* -------- 用户空间调用栈 -------- */
    memset(entries, 0, sizeof(entries));

    /*
     * 注意:save_stack_trace_user() 只能对 current 生效,
     * 会保存从用户态进入内核时的那一组返回地址。
     */
    nr = stack_trace_save_user(entries, ARRAY_SIZE(entries));

    pr_info(&#34;user backtrace (raw user PCs):\n&#34;);
    for (i = 0; i < nr; i++) {
        /*
         * 这些是用户虚拟地址,需要 offline 用 addr2line 等工具
         * 结合 /proc//maps 的映射基址还原成具体函数名/行号。
         */
        pr_info(&#34;  [u%02d] 0x%016lx\n&#34;, i, entries[i]);
    }
#else
    pr_info(&#34;user stack trace not supported &#34;
            &#34;(CONFIG_USER_STACKTRACE_SUPPORT is disabled)\n&#34;);
#endif /* CONFIG_USER_STACKTRACE_SUPPORT */
}
#else
static inline void dump_all_stacks(void) {}
#endif /* CONFIG_STACKTRACE */

可以在目标位置插入调用dump_all_stacks()。 抓内核日志。

解析用户态调用栈

准备了一个python脚本来解析用户态的堆栈,该脚本会计算堆栈中地址相对于符号文件的偏移,并用addr2line尝试解析符号。

python3 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
&#34;&#34;&#34;
binder_user_stack_resolver.py

用法示例:

1) 先把内核日志导出来:
   adb shell dmesg | grep binder_open > binder.log

2) 把 /proc//maps 导出来:
   adb shell cat /proc/1234/maps > maps_1234.txt

3) 在宿主机上执行(假设已提取 system 映像到 /path/to/symbols):

   python3 binder_user_stack_resolver.py \
       --pid 1234 \
       --log binder.log \
       --maps maps_1234.txt \
       --sym-root /path/to/symbols \
       --addr2line aarch64-linux-android-addr2line

说明:
- sym-root 目录下应当能找到类似 /system/bin/surfaceflinger 这样的路径
  (例如解包 system.img 后的根目录)
- addr2line 可以是 aarch64-linux-android-addr2line 或 llvm-addr2line 等
&#34;&#34;&#34;

import argparse
import os
import re
import subprocess
import sys
from typing import List, Tuple, Dict, Optional


class MapEntry:
    def __init__(self, start: int, end: int, offset: int, path: str):
        self.start = start
        self.end = end
        self.offset = offset
        self.path = path

    def contains(self, addr: int) -> bool:
        return self.start <= addr < self.end

    def file_offset(self, addr: int) -> int:
        &#34;&#34;&#34;
        计算 addr 在 ELF 文件内的偏移:
        file_off = (addr - start_vma) + file_offset_column
        &#34;&#34;&#34;
        return (addr - self.start) + self.offset


def parse_maps(maps_path: str) -> List[MapEntry]:
    entries: List[MapEntry] = []
    with open(maps_path, &#34;r&#34;, encoding=&#34;utf-8&#34;, errors=&#34;ignore&#34;) as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            # 形如:
            # start-end perms offset dev inode pathname
            # 0000007f8a100000-0000007f8a200000 r-xp 00000000 08:01 123456 /system/bin/surfaceflinger
            parts = line.split()
            if len(parts) < 5:
                continue

            addr_range = parts[0]
            perms = parts[1]
            offset_str = parts[2]
            path = parts[5] if len(parts) >= 6 else &#34;&#34;

            # 只关心可执行映射
            if &#34;x&#34; not in perms:
                continue

            try:
                start_str, end_str = addr_range.split(&#34;-&#34;)
                start = int(start_str, 16)
                end = int(end_str, 16)
                offset = int(offset_str, 16)
            except ValueError:
                continue

            if not path or path == &#34;[vdso]&#34; or path.startswith('['):
                continue

            entries.append(MapEntry(start, end, offset, path))
    return entries


def find_mapping(maps: List[MapEntry], addr: int) -> Optional[MapEntry]:
    for m in maps:
        if m.contains(addr):
            return m
    return None


def resolve_addr(addr: int, maps: List[MapEntry], sym_root: str, addr2line_bin: str) -> str:
    m = find_mapping(maps, addr)
    if not m:
        return f&#34;0x{addr:016x} &#34;

    file_off = m.file_offset(addr)

    # 把 /system/bin/surfaceflinger 这种路径映射到 sym_root/system/bin/surfaceflinger
    rel_path = m.path.lstrip(&#34;/&#34;)  # 去掉开头的 '/'
    elf_path = os.path.join(sym_root, rel_path)

    if not os.path.exists(elf_path):
        return f&#34;0x{addr:016x} {m.path}+0x{file_off:x} (ELF not found: {elf_path})&#34;

    cmd = [addr2line_bin, &#34;-C&#34;, &#34;-f&#34;, &#34;-e&#34;, elf_path, f&#34;0x{file_off:x}&#34;]
    try:
        out = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
        decoded = out.decode(&#34;utf-8&#34;, errors=&#34;ignore&#34;).strip().splitlines()
        if len(decoded) >= 2:
            func = decoded[0]
            loc = decoded[1]
            return f&#34;0x{addr:016x} {m.path}+0x{file_off:x} => {func} @ {loc}&#34;
        elif decoded:
            return f&#34;0x{addr:016x} {m.path}+0x{file_off:x} => {decoded[0]}&#34;
        else:
            return f&#34;0x{addr:016x} {m.path}+0x{file_off:x} (no addr2line output)&#34;
    except subprocess.CalledProcessError as e:
        return f&#34;0x{addr:016x} {m.path}+0x{file_off:x} (addr2line error: {e})&#34;


def parse_log_for_pid(log_path: str, pid: int) -> List[int]:
    &#34;&#34;&#34;
    从 binder log 中抽取指定 pid 的 [uXX] 行的地址
    &#34;&#34;&#34;
    addrs: List[int] = []
    current_pid: Optional[int] = None
    in_user_bt = False

    pid_line_re = re.compile(r&#34;binder_open:\s+pid=(\d+)\s+comm=&#34;)
    user_bt_re = re.compile(r&#34;binder_open:\s+user backtrace&#34;)
    addr_re = re.compile(r&#34;\[u\d+\]\s+0x([0-9a-fA-F]+)&#34;)

    with open(log_path, &#34;r&#34;, encoding=&#34;utf-8&#34;, errors=&#34;ignore&#34;) as f:
        for line in f:
            line = line.rstrip(&#34;\n&#34;)

            m = pid_line_re.search(line)
            if m:
                current_pid = int(m.group(1))
                in_user_bt = False
                continue

            if current_pid == pid and user_bt_re.search(line):
                in_user_bt = True
                continue

            if in_user_bt and current_pid == pid:
                m2 = addr_re.search(line)
                if m2:
                    addr_str = m2.group(1)
                    addr = int(addr_str, 16)
                    addrs.append(addr)
                else:
                    # 遇到不是 [uXX] 的行,认为本次 user backtrace 结束
                    if line.strip() == &#34;&#34; or line.startswith(&#34;binder_open:&#34;):
                        in_user_bt = False

    return addrs


def main():
    parser = argparse.ArgumentParser(description=&#34;Resolve SurfaceFlinger binder_open user stacks&#34;)
    parser.add_argument(&#34;--pid&#34;, type=int, required=True, help=&#34;surfaceflinger 的 pid&#34;)
    parser.add_argument(&#34;--log&#34;, required=True, help=&#34;包含 binder_open 打印的 log 文件&#34;)
    parser.add_argument(&#34;--maps&#34;, required=True, help=&#34;/proc//maps 导出的文件路径&#34;)
    parser.add_argument(&#34;--sym-root&#34;, required=True, help=&#34;带符号 system/so/root 的根目录&#34;)
    parser.add_argument(&#34;--addr2line&#34;, default=&#34;addr2line&#34;,
                        help=&#34;addr2line 可执行文件名(默认: addr2line,可改为 aarch64-linux-android-addr2line)&#34;)

    args = parser.parse_args()

    maps = parse_maps(args.maps)
    if not maps:
        print(&#34;ERROR: no executable mappings parsed from maps file&#34;, file=sys.stderr)
        sys.exit(1)

    addrs = parse_log_for_pid(args.log, args.pid)
    if not addrs:
        print(f&#34;ERROR: no user backtrace addresses found in log for pid={args.pid}&#34;, file=sys.stderr)
        sys.exit(1)

    print(f&#34;# binder_open user stack for pid={args.pid}&#34;)
    for addr in addrs:
        resolved = resolve_addr(addr, maps, args.sym_root, args.addr2line)
        print(resolved)


if __name__ == &#34;__main__&#34;:
    main()

解析示例

原理分析

1. 发生系统调用时保存现场

以用户空间执行 open(&#34;34;/dev/binder&#34;;) 的系统调用为例,指令是: svc #0

CPU 硬件做的事情:

  • 当 EL0 执行 svc #imm:
  • CPU 切换异常级:EL0 → EL1;
  • 用户 PSTATE 保存到 SPSR_EL1
  • 用户 PC(svc 下一条指令地址)保存到 ELR_EL1
  • 使用异常向量表中 EL0 同步异常的入口地址(VBAR_EL1 指向的一张表):
  • 跳转到 el0_sync(或类似名字)的入口
  • 切换栈指针:
  • 使用 SP_EL1 作为栈指针(这时已经是内核栈)
  • SP_EL0 保留的是用户态栈,暂时不会动

注意:此时 x0--x30 里的值仍然是用户态的寄存器值,CPU 没帮你保存到内存,必须靠内核汇编自己存。

assemble 复制代码
arch/arm64/kernel/head.S
__HEAD
    primary_entry
        __primary_switched
            adr_l	x8, vectors			// load VBAR_EL1 with virtual
            msr	        vbar_el1, x8			// vector table address

vectors填入vbar_el1寄存器中,其中vectors是一个全局标记:

less 复制代码
arch/arm64/kernel/entry.S
/*
 * Exception vectors.
 */
	.pushsection &#34;.entry.text&#34;, &#34;ax&#34;

	.align	11
SYM_CODE_START(vectors)
	kernel_ventry	0, t, 64, sync		// Synchronous 64-bit EL0
SYM_CODE_END(vectors)

vectors处通过kernel_ventry定义了多个入口:

less 复制代码
.macro kernel_ventry, el:req, ht:req, regsize:req, label:req
.align 7
sub	sp, sp, #PT_REGS_SIZE
b	el\el\ht\()_\regsize\()_\label

先预留PT_REGS_SIZE大小的栈空间,然后跳转到el0t_64_sync处执行

scss 复制代码
SYM_CODE_START_LOCAL(el\el\ht\()_\regsize\()_\label)
	kernel_entry \el, \regsize
	mov	x0, sp
	bl	el\el\ht\()_\regsize\()_\label\()_handler
	.if \el == 0
	b	ret_to_user
	.else
	b	ret_to_kernel
	.endif
SYM_CODE_END(el\el\ht\()_\regsize\()_\label)
	.endm

kernel_entry \el, \regsize宏处保存寄存器信息到内核栈中

csharp 复制代码
.macro	kernel_entry, el, regsize = 64
	stp	x0, x1, [sp, #16 * 0]
	stp	x2, x3, [sp, #16 * 1]
	stp	x4, x5, [sp, #16 * 2]
	stp	x6, x7, [sp, #16 * 3]
	stp	x8, x9, [sp, #16 * 4]
	stp	x10, x11, [sp, #16 * 5]
	stp	x12, x13, [sp, #16 * 6]
	stp	x14, x15, [sp, #16 * 7]
	stp	x16, x17, [sp, #16 * 8]
	stp	x18, x19, [sp, #16 * 9]
	stp	x20, x21, [sp, #16 * 10]
	stp	x22, x23, [sp, #16 * 11]
	stp	x24, x25, [sp, #16 * 12]
	stp	x26, x27, [sp, #16 * 13]
	stp	x28, x29, [sp, #16 * 14]

	.if	\el == 0
	clear_gp_regs
	mrs	x21, sp_el0
	ldr_this_cpu	tsk, __entry_task, x20
	msr	sp_el0, tsk
	.else
	add	x21, sp, #PT_REGS_SIZE
	get_current_task tsk
	.endif /* \el == 0 */
	mrs	x22, elr_el1
	mrs	x23, spsr_el1
	stp	lr, x21, [sp, #S_LR]

	/*
	 * For exceptions from EL0, create a final frame record.
	 * For exceptions from EL1, create a synthetic frame record so the
	 * interrupted code shows up in the backtrace.
	 */
	.if \el == 0
	stp	xzr, xzr, [sp, #S_STACKFRAME] //  pt_regs 区域里写一个终止 frame record(FP、LR = 0),方便 unwinder 知道到头了。
	.else
	stp	x29, x22, [sp, #S_STACKFRAME]
	.endif
	add	x29, sp, #S_STACKFRAME

之后栈布局如下

go 复制代码
低地址
+-------------------------+
| struct thread_info      |  flags & PF_KTHREAD)
	return 0;

arch_stack_walk_user(consume_entry, &c, task_pt_regs(current));

return c.len;

} #endif

javascript 复制代码
`arch_stack_walk_user`是一个架构相关的函数,定义在`arch/arm64/kernel/stacktrace.c`中:

    void arch_stack_walk_user(stack_trace_consume_fn consume_entry, void *cookie,
    					const struct pt_regs *regs)
    {
    	if (!consume_entry(cookie, regs->pc))
    		return;

    	if (!compat_user_mode(regs)) {
    		/* AARCH64 mode */
    		struct frame_tail __user *tail;

    		tail = (struct frame_tail __user *)regs->regs[29];
    		while (tail && !((unsigned long)tail & 0x7))
    			tail = unwind_user_frame(tail, cookie, consume_entry);
    	} else {
                    /* ...... */
    	}
    }

`task_pt_regs(current) `

    #define task_pt_regs(p) \
    	((struct pt_regs *)(THREAD_SIZE + task_stack_page(p)) - 1)



    /*
     * When accessing the stack of a non-current task that might exit, use
     * try_get_task_stack() instead.  task_stack_page will return a pointer
     * that could get freed out from under you.
     */
    static __always_inline void *task_stack_page(const struct task_struct *task)
    {
    	return task->stack;
    }

其中`THREAD_SIZE` 是内核栈的大小,`task_stack_page`拿到的是栈的地址。
因此`task_pt_regs(current)`拿到的就是当前进程内核栈上保存的中断线程,然后 `arch_stack_walk_user`从中找到`x29`寄存器,并依次去找`lr`指针和`fp`指针,就可以抓到用户态的调用栈。
相关推荐
Joren的学习记录2 小时前
【Linux运维进阶知识】Nginx负载均衡
linux·运维·nginx
用户2190326527352 小时前
Java后端必须的Docker 部署 Redis 集群完整指南
linux·后端
里纽斯4 小时前
RK平台Watchdog硬件看门狗验证
android·linux·rk3588·watchdog·看门狗·rk平台·wtd
chem41114 小时前
魔百盒 私有网盘seafile搭建
linux·运维·网络
早睡的叶子4 小时前
VM / IREE 的调度器架构
linux·运维·架构
兄台の请冷静5 小时前
linux 安装sentinel 并加入systemctl
linux·运维·sentinel
skywalk81635 小时前
postmarketos一个专为智能手机和平板设备设计的开源 Linux 发行版 支持红米2
linux·智能手机·电脑
青梅煮久5 小时前
RK3566 Linux实例应用(1)——环境编译与烧录
linux·数据库·php
gcw10245 小时前
好用的CRON表达式工具
java·linux·运维·服务器·spring