/*
 * cmd_call.c - 同步函数调用命令模块（call <func>(args)）
 *
 * gdb infcall 语义：在目标 .text 的 NOP 对齐填充区放置返回断点(BRK#0)，
 * 注入目标函数并同步等待其返回，成功后收取返回值(x0)并恢复调用前
 * 寄存器快照，目标保持 STOPPED（'c' 继续）；'call' 命令会自动 resume。
 *
 * 依赖 thread_ops.c(ptrace/寄存器)、mem_rw.c(内存读写)、symbol_parse.c
 * (符号查找)、cmd_struct.c(出参 scratch 符号)。
 */

#include "lnxall_trace.h"

/* 调用约定特征（参数寄存器数/栈对齐/LR 寄存器等）统一由 arch_port.h 提供；
 * 寄存器访问一律走 arch_reg_* 接口，见 arch_port.h/arch_port.c。 */

/* ========================================================================
 * Return-breakpoint placement — gdb-style NOP-padding slot
 *
 * gdb's create_call_dummy() never parks the return dummy in *live* code:
 * the trap must live in dead code so neither the callee nor a sibling
 * thread can ever execute it while our BRK is in place.  We scan the
 * target executable's own .text for runs of NOP alignment padding
 * (compiler/linker fill between functions) and overwrite one NOP with
 * the BRK.  Padding NOPs sit *outside* every function body, so nothing
 * can reach them in normal execution.
 * ======================================================================== */

#define CALL_SCAN_CHUNK 65536       /* bulk-read chunk when scanning .text */
#define CALL_MAX_MAPS   32          /* max executable code mappings to scan */

/* 目标进程注入的专用 NOP 桩函数名（src/trace_debug.c 定义）：函数体为
 * 连续 NOP 指令、无任何调用点，是返回断点的理想驻留点；call 命令优先
 * 在此查找槽位，符号缺失/内容非 NOP 时回退到 .text 尾部填充扫描 */
#define TRACE_DBG_NOP_STUB_NAME    "trace_dbg_nop_stub"
#define TRACE_DBG_STUB_PROBE_LEN   64  /* 符号 size 未知（如 dynsym）时的探测长度 */

/* glibc only declares process_vm_readv under _GNU_SOURCE, so provide the
 * prototype locally (identical to the glibc one). */
extern ssize_t process_vm_readv(pid_t __pid, const struct iovec *__lvec,
                                unsigned long __liovcnt,
                                const struct iovec *__rvec,
                                unsigned long __riovcnt,
                                unsigned long __flags);

/**
 * @brief 批量读取目标（已停止）进程内存
 *
 * 优先走 process_vm_readv 快速路径，失败时回退到 ptrace 单字读取。
 *
 * @param[in]  pid  目标进程
 * @param[in]  addr 目标地址
 * @param[out] buf  输出缓冲区
 * @param[in]  len  读取字节数
 * @return 实际读取字节数；-1 表示硬失败
 */
static long call_read_vm(pid_t pid, unsigned long addr, void *buf, size_t len)
{
    struct iovec local  = { buf, len };
    struct iovec remote = { (void *)addr, len };
    ssize_t n = process_vm_readv(pid, &local, 1, &remote, 1, 0);
    if (n >= 0) return (long)n;
    if (mem_read(pid, addr, buf, len) == 0) return (long)len;
    return -1;
}

/**
 * @brief 判断 [addr, addr+ARCH_BP_INSN_LEN) 是否落在某个已知函数体内
 *
 * 对齐填充位于函数*之间*，绝不会在函数体内；此即"无人会执行"的判据。
 * 覆盖长度用断点指令真实宽度而非硬编码 4：arm32 的 Thumb BKPT 仅 2
 * 字节，恒按 +4 判断虽偏保守（槽位紧贴函数尾时被误拒，无安全性问题），
 * 但会白白丢弃合法槽位，按 ARCH_BP_INSN_LEN 对齐实际写入长度更准确。
 *
 * @param[in] addr 待判断地址
 * @return 1 在函数体内，0 不在
 */
static int call_slot_in_func(unsigned long addr)
{
    for (int i = 0; i < g_dbg.sym_count; i++) {
        const symbol_t *s = &g_dbg.sym_table[i];
        if (s->type != SYM_FUNC || s->size == 0) continue;
        if (addr >= s->addr && addr + ARCH_BP_INSN_LEN <= s->addr + s->size)
            return 1;
    }
    return 0;
}

/**
 * @brief 评估扫描到的 NOP 段 [lo, hi)（映射内偏移）
 *
 * 段长度至少一条指令且不落在任何函数体内即可用。
 *
 * @param[in]  map_start 映射起始地址
 * @param[in]  lo        段起始偏移
 * @param[in]  hi        段结束偏移
 * @param[out] slot_out  命中时输出槽位地址
 * @return 1 可用，0 不可用
 */
static int call_consider_run(unsigned long map_start, long lo, long hi,
                             unsigned long *slot_out)
{
    if (lo < 0 || hi - lo < ARCH_BP_INSN_LEN) return 0;
    unsigned long slot = map_start + (unsigned long)lo;
    if (!call_slot_in_func(slot)) {
        *slot_out = slot;
        return 1;
    }
    return 0;
}

/**
 * @brief 在目标进程的 trace_dbg_nop_stub 桩函数内查找 NOP 槽位
 *
 * 该桩函数由目标程序注入（src/trace_debug.c 的 trace_dbg_nop_stub），
 * 函数体为连续 NOP 指令、无人调用（死代码），将返回断点 BRK 覆盖其一
 * 可保证正常执行永不触发。从函数起始逐条验证 NOP（规避编译器序言/
 * BTI 跳板等非 NOP 前缀指令），取首个长度达到断点指令长的连续 NOP run
 * 起始地址。
 *
 * @return 指令对齐（ARCH_NOP_STEP）的槽位地址；0 表示符号缺失、
 *         非函数符号、读取失败或函数体内无足够 NOP run
 */
static unsigned long call_find_stub_nop_slot(void)
{
    symbol_t *sym = find_symbol_any(TRACE_DBG_NOP_STUB_NAME);
    if (!sym || sym->type != SYM_FUNC) {
        DBG("nop_slot: stub symbol '%s' unavailable, fall back to .text scan",
            TRACE_DBG_NOP_STUB_NAME);
        return 0;
    }

    /* 探测长度：优先符号 size，未知（如 dynsym 导出符号 size 为 0）时
     * 用固定长度，覆盖桩函数全部 NOP 体且不跨页越界 */
    size_t probe = (sym->size > 0) ? (size_t)sym->size : TRACE_DBG_STUB_PROBE_LEN;
    unsigned char *buf = malloc(probe);
    if (!buf) return 0;

    long n = call_read_vm(g_dbg.target_pid, sym->addr, buf, probe);
    if (n <= 0) {
        free(buf);
        DBG("nop_slot: failed to read stub @ 0x%lx, fall back to .text scan",
            sym->addr);
        return 0;
    }

    size_t n4 = (size_t)n;
    n4 -= n4 % ARCH_NOP_STEP;      /* whole instructions only */
    long run_start = -1;
    unsigned long slot = 0;
    for (size_t off = 0; off + ARCH_NOP_STEP <= n4; off += ARCH_NOP_STEP) {
        if (arch_buf_is_nop(buf, off)) {
            if (run_start < 0) run_start = (long)off;
            if ((long)(off + ARCH_NOP_STEP - (size_t)run_start) >=
                ARCH_BP_INSN_LEN) {
                slot = sym->addr + (unsigned long)run_start;
                break;
            }
        } else {
            run_start = -1;
        }
        
    }
    free(buf);
    DBG("nop_slot: stub '%s' @ 0x%lx -> slot 0x%lx",
        TRACE_DBG_NOP_STUB_NAME, sym->addr, slot);
    return slot;
}

/**
 * @brief 为返回断点查找 NOP 填充槽位
 *
 * 优先使用目标进程注入的专用 NOP 桩函数 trace_dbg_nop_stub（函数体为
 * 连续 NOP、无人调用，比 .text 尾部填充更确定）；找不到（符号缺失/
 * 内容非 NOP/读取失败）时回退到下方 .text 尾部扫描。
 *
 * 回退逻辑：扫描目标可执行文件 r-xp 映射中连续 NOP 段，从代码段末尾
 * *向前*搜索：.text 尾部多为链接器填充，首个分块内几乎必能命中可用
 * 槽位。候选须：
 *   - 至少一条断点指令长（ARCH_BP_INSN_LEN）
 *   - 不在任何已知函数体内
 *
 * 槽位与当前 PC 无关，每次 'call' 动态重算（只读 .text 尾部，开销可忽略）。
 *
 * @param[in] pid 目标进程
 * @return 指令对齐（ARCH_NOP_STEP）的槽位地址（首个命中，取高地址端）；
 *         0 表示无可用填充（调用方须视为硬错误并中止本次 call）
 */
static unsigned long call_find_nop_slot(pid_t pid)
{
    char mpath[64];
    FILE *fp;
    char line[512];
    unsigned char *buf;
    unsigned long slot = 0;

    /* the executable's code mappings, in address order */
    struct { unsigned long start, end; } maps[CALL_MAX_MAPS];
    int n_maps = 0;

    if (g_dbg.exe_path[0] == '\0') return 0;

    /* 优先使用目标进程注入的专用 NOP 桩函数 trace_dbg_nop_stub：
     * 函数体为连续 NOP 且无人调用，比 .text 尾部填充更确定；
     * 符号缺失/内容非 NOP/读取失败时返回 0，回退到下方扫描 */
    slot = call_find_stub_nop_slot();
    if (slot != 0) return slot;

    snprintf(mpath, sizeof(mpath), "/proc/%d/maps", pid);
    fp = fopen(mpath, "r");
    if (!fp) { perror("fopen /proc/pid/maps"); return 0; }
    buf = malloc(CALL_SCAN_CHUNK);
    if (!buf) { fclose(fp); return 0; }

    while (fgets(line, sizeof(line), fp)) {
        unsigned long start, end;
        char perms[8];
        char *p;

        if (sscanf(line, "%lx-%lx %7s", &start, &end, perms) != 3) continue;
        if (!strchr(perms, 'x')) continue;          /* not code */
        p = strstr(line, g_dbg.exe_path);
        if (!p) continue;                           /* not our exe */
        {
            char c = p[strlen(g_dbg.exe_path)];
            if (!(c == '\n' || c == '\r' || c == '\0' || c == ' ')) continue;
        }
        if (n_maps < CALL_MAX_MAPS) {
            maps[n_maps].start = start;
            maps[n_maps].end = end;
            n_maps++;
        }
    }
    fclose(fp);

    /* Walk the code mappings from the highest address down, so the tail of
     * .text (padding) is inspected first and we can bail out on the first
     * usable run instead of scanning the whole segment. */
    for (int m = n_maps - 1; m >= 0; m--) {
        unsigned long map_start = maps[m].start;
        size_t map_len = (size_t)(maps[m].end - map_start);
        long run_lo = -1, run_hi = -1;   /* NOP run in progress (offsets) */
        size_t base = map_len;           /* high watermark, step-aligned   */

        DBG("nop_slot: scanning exe .text 0x%lx-0x%lx backward",
            maps[m].start, maps[m].end);

        while (base >= ARCH_NOP_STEP) {
            size_t chunk = (base > CALL_SCAN_CHUNK) ? CALL_SCAN_CHUNK : base;
            chunk -= chunk % ARCH_NOP_STEP;    /* keep instruction alignment */
            size_t from = base - chunk;
            long n = call_read_vm(pid, map_start + from, buf, chunk);
            if (n <= 0) {                /* unreadable: close and give up   */
                if (call_consider_run(map_start, run_lo, run_hi, &slot))
                    goto found;
                break;
            }

            size_t n4 = (size_t)n;
            n4 -= n4 % ARCH_NOP_STEP;   /* whole instructions only         */
            if (n4 < chunk) {               /* unreadable top: hard edge    */
                if (call_consider_run(map_start, run_lo, run_hi, &slot))
                    goto found;
                run_lo = run_hi = -1;
            }

            /* scan the chunk backwards, instruction by instruction */
            if (n4 >= ARCH_NOP_STEP) {
                for (long o = (long)from + (long)n4 - ARCH_NOP_STEP;
                     ; o -= ARCH_NOP_STEP) {
                    if (arch_buf_is_nop(buf, (size_t)(o - (long)from))) {
                        if (run_lo < 0) {
                            run_lo = o;
                            run_hi = o + ARCH_NOP_STEP;
                        } else { run_lo = o; }   /* extend down */
                    } else if (run_lo >= 0) {
                        if (call_consider_run(map_start, run_lo, run_hi, &slot))
                            goto found;
                        run_lo = run_hi = -1;
                    }
                    if (o == (long)from) break;
                }
            }
            base = from;
        }
        /* run reaching the start of the mapping */
        if (call_consider_run(map_start, run_lo, run_hi, &slot))
            goto found;
    }

    free(buf);
    DBG("nop_slot: no usable slot found");
    return 0;

found:
    free(buf);
    DBG("nop_slot: slot 0x%lx", slot);
    return slot;
}

/**
 * @brief 用架构断点指令（BRK/BKPT/INT3）覆盖 addr 处的指令
 *
 * @param[in]  pid     目标进程
 * @param[in]  addr    覆盖地址
 * @param[out] orig    原始指令字节（恢复用）
 * @param[out] bp_len  断点长度（由 arch_port 层给出）
 * @return 0 成功，-1 失败
 */
static int call_write_break(pid_t pid, unsigned long addr,
                            unsigned char *orig, size_t *bp_len)
{
    int n = arch_break_insn_len();
    if (mem_read(pid, addr, orig, (size_t)n) != 0) return -1;
    if (mem_write(pid, addr, arch_break_insn(), (size_t)n) != 0) return -1;
    *bp_len = (size_t)n;
    return 0;
}

/**
 * @brief 判断目标是否停在我们注入的返回断点上
 *
 * 断点命中时的 PC 相对断点地址的偏移由架构决定：
 * BRK（aarch64）/BKPT（arm32）停在断点指令本身（偏移 0）；
 * INT3（x86）RIP 指向断点指令之后 1 字节（偏移 1，见 ARCH_BP_PC_DELTA）。
 *
 * @param[in] r        寄存器快照
 * @param[in] bp_addr  断点地址
 * @return 1 命中，0 未命中
 */
int call_hit_break(const arch_regs_t *r, unsigned long bp_addr)
{
    unsigned long long pc = arch_reg_pc(r);
    unsigned long long hit = (unsigned long long)bp_addr + ARCH_BP_PC_DELTA;
    return pc == hit;
}

/**
 * @brief 恢复 call_write_break 覆盖的原始指令字节
 *
 * @return 0（恒成功，与头文件原型一致）
 */
int call_restore_break(void)
{
    if (g_dbg.breakpoint_len > 0) {
        unsigned char *b = g_dbg.breakpoint_orig;
        int n = g_dbg.breakpoint_len;
        DBG("restore_break: addr=0x%lx len=%d orig=%02x %02x %02x %02x "
            "%02x %02x %02x %02x",
            g_dbg.breakpoint_addr, n,
            n > 0 ? b[0] : 0, n > 1 ? b[1] : 0, n > 2 ? b[2] : 0,
            n > 3 ? b[3] : 0, n > 4 ? b[4] : 0, n > 5 ? b[5] : 0,
            n > 6 ? b[6] : 0, n > 7 ? b[7] : 0);
        mem_write(g_dbg.target_pid, g_dbg.breakpoint_addr,
                  g_dbg.breakpoint_orig, (size_t)g_dbg.breakpoint_len);
    }
    return 0;
}

/**
 * @brief 完成一次已命中返回断点的注入调用
 *
 * 调用方已确认 pid 停在注入的 BRK 上（PC == breakpoint_addr）。
 * 收取返回值(x0)，恢复被覆盖的指令与调用前完整寄存器快照，
 * 目标保持 STOPPED；cmd_call_function 返回后自动 resume。
 *
 * @param[in] pid 目标进程
 * @return 1 = 调用完成，0 = 停止与本调用无关，-1 = 错误
 */
int complete_pending_call(pid_t pid)
{
    arch_regs_t cur;
    if (arch_get_regs(pid, &cur) != 0) return -1;
    if (!call_hit_break(&cur, g_dbg.breakpoint_addr)) return 0;

    unsigned long long rv = arch_reg_ret(&cur);
    g_dbg.call_return_value = (unsigned long)rv;
    g_dbg.call_has_retval = 1;

    /* restore the overwritten instruction, then ALL registers (the callee
     * may clobber SP/FP and callee-saved registers).  Do NOT resume: the
     * target stays stopped at its pre-call PC, ready for the 'c' command —
     * this is exactly what gdb does after an infcall. */
    call_restore_break();
    arch_set_regs(pid, &g_dbg.saved_regs);
    g_dbg.call_in_progress = 0;
    g_dbg.breakpoint_len = 0;
    g_dbg.call_sync_done = 1;
    return 1;
}

/* ========================================================================
 * Synchronous function-call completion (gdb infcall semantics)
 *
 * cmd_call_function() launches the call and then blocks inside
 * call_wait_for_return() until the injected return-breakpoint (BRK) fires
 * or the call fails/times out.  On success the return value is harvested
 * from x0 and the full pre-call register snapshot is restored, with the
 * target left STOPPED — the user resumes it with 'c' (exactly like gdb
 * leaves the inferior stopped after a call expression).
 * ======================================================================== */

/**
 * @brief 中止进行中的调用
 *
 * 恢复被覆盖的返回断点指令与调用前完整寄存器快照，目标保持 STOPPED；
 * 'c' 命令负责恢复运行。
 */
void abort_call_and_restore(void)
{
    call_restore_break();
    arch_set_regs(g_dbg.target_pid, &g_dbg.saved_regs);
    g_dbg.call_in_progress = 0;
    g_dbg.breakpoint_len = 0;
    g_dbg.call_sync_done = 1;   /* target stopped & state restored; cmd_call_function auto-resumes */
    g_dbg.call_stopped_in_frame = 0;
}

/**
 * @brief 中止一次调用但保持目标停在当前帧
 *        (gdb `unwind-on-timeout off' / `unwind-on-signal off' 语义)
 *
 * 仅恢复被覆盖的返回断点指令,不回退寄存器(PC/SP/LR)。若被调函数已进入
 * 锁临界区,锁状态与执行流保持一致:resume 后函数可自然完成并释放锁,
 * 避免 "PC 回退跳过解锁代码 → rwlock 永久残留 → 之后所有调用全部超时"
 * 的锁污染。目标保持 STOPPED,由用户决定 'c' 继续(函数可能返回已恢复的
 * NOP 槽,有风险)或重启目标进程。
 */
void abort_call_keep_frame(void)
{
    call_restore_break();
    g_dbg.call_in_progress = 0;
    g_dbg.breakpoint_len = 0;
    g_dbg.call_sync_done = 1;          /* target stopped INSIDE callee */
    g_dbg.call_stopped_in_frame = 1;
}

/**
 * @brief 同步等待注入调用到达返回断点
 *
 * 等价于 gdb 的 proceed() + wait_for_inferior()：在 'call' 命令内阻塞
 * 直至被调函数返回（或失败），结果由同一命令上报，目标绝不会滞留在
 * 断点上。
 *
 * @return  1 = 调用完成（状态已恢复，cmd_call_function 自动 resume）
 *          2 = 目标停在调用帧内（无关 SIGTRAP 或致命信号，帧保留）
 *          3 = 调用被 Ctrl-C 中断（帧保留）
 *         -1 = 调用期间目标退出或被杀死
 */
int call_wait_for_return(void)
{
    int status;

    /* No built-in timeout (gdb native target: `set direct-call-timeout
     * unlimited' default) — wait until the callee returns, faults, the
     * target exits, or the user presses Ctrl-C. */
    while (1) {
        /* User Ctrl-C during the call (gdb semantics): interrupt the target
         * and keep it stopped INSIDE the callee — no PC rewind, lock state
         * intact, exactly like `unwind-on-timeout off'. */
        if (g_intr_requested) {
            g_intr_requested = 0;
            fprintf(stderr, "Error: %s() interrupted by Ctrl-C; "
                    "interrupting and keeping the target in its current frame\n",
                    g_dbg.call_func_name);
            ptrace(PTRACE_INTERRUPT, g_dbg.target_pid, NULL, NULL);
            for (int i = 0; i < 50; i++) {
                pid_t w = waitpid(g_dbg.target_pid, &status, WNOHANG | __WALL);
                if (w == g_dbg.target_pid) break;
                if (w == -1) break;
                usleep(10000);
            }
            /* Also freeze the siblings that stayed running during the call
             * so the abort keeps the whole process stopped.  Their execution
             * is NOT rewound — unlike the leader they keep their own PC/SP,
             * so locks they hold are released normally once resumed. */
            stop_all_threads();
            abort_call_keep_frame();
            return 3;
        }

        /* waitpid(-1): reap stop events from EVERY tracee, not just the
         * leader.  Siblings that receive a signal during the call stop in
         * signal-delivery-stop, and forked children stop on
         * PTRACE_EVENT_EXEC; if we only waitpid(target_pid) those events
         * are never consumed and the threads freeze in tracing stop ('t')
         * forever — exactly the field hang where a frozen sibling held the
         * rwlock the callee needed.  Handle_side_thread_stop() reinjects
         * benign signals and forwards fork/clone/exec events to
         * handle_child_status(), so nothing wedges. */
        pid_t w = waitpid(-1, &status, WNOHANG | __WALL);
        if (w == -1) {
            if (errno == EINTR) continue;
            perror("waitpid"); return -1;
        }
        if (w != g_dbg.target_pid) {         /* w == 0: still running */
            if (w > 0) {
                /* 兄弟/新线程/子进程的停止事件：统一处理而非丢弃，否则
                 * infcall 期间新建线程（pthread_create）、收到信号的兄弟
                 * 线程、以及 fork 出的子进程（exec 事件）会永久卡死。 */
                handle_side_thread_stop(w, status);
                continue;
            }
            usleep(2000);
            continue;
        }

        if (!WIFSTOPPED(status)) {
            if (WIFSIGNALED(status))
                fprintf(stderr, "Error: target killed by signal %d during %s()\n",
                        WTERMSIG(status), g_dbg.call_func_name);
            else
                fprintf(stderr, "Error: target exited during %s()\n",
                        g_dbg.call_func_name);
            g_dbg.is_attached = 0;
            g_dbg.target_pid = 0;
            g_dbg.call_in_progress = 0;
            return -1;
        }

        int sig = WSTOPSIG(status);
        unsigned int ev = (unsigned int)status >> 16;

        if (ev == PTRACE_EVENT_STOP) {
            /* We only see this if the Ctrl-C interrupt raced with the return
             * breakpoint.  Probe whether it landed on our breakpoint anyway. */
            arch_regs_t cur;
            if (arch_get_regs(g_dbg.target_pid, &cur) == 0 &&
                call_hit_break(&cur, g_dbg.breakpoint_addr)) {
                /* Freeze the siblings still running during the call before
                 * touching target state (gdb stop_all_threads semantics). */
                stop_all_threads();
                complete_pending_call(g_dbg.target_pid);
                return 1;
            }
            abort_call_keep_frame();
            return 2;
        }
        if (ev != 0) {
            /* fork/clone/exec/exit ptrace event during the call */
            handle_child_status(g_dbg.target_pid, status);
            continue;
        }
        if (sig == SIGTRAP) {
            /* Freeze siblings still running during the call before probing
             * the breakpoint / restoring state (gdb stop_all_threads). */
            stop_all_threads();
            if (complete_pending_call(g_dbg.target_pid) == 1)
                return 1;
            /* SIGTRAP that is not our return-breakpoint: keep the target
             * stopped inside the callee (no PC rewind, lock state intact)
             * instead of reinjecting the signal — reinjecting SIGTRAP would
             * kill the target (it has no handler). */
            fprintf(stderr, "Error: target stopped on unexpected SIGTRAP "
                    "during %s(); keeping target in its current frame\n",
                    g_dbg.call_func_name);
            abort_call_keep_frame();
            return 2;
        }
        if (sig == SIGSEGV || sig == SIGBUS || sig == SIGILL ||
            sig == SIGFPE || sig == SIGABRT) {
            /* Fatal fault inside the callee: keep the target stopped in the
             * faulting frame — gdb `unwind-on-signal off' (default) semantics.
             * Do NOT rewind the PC: rewinding across a critical section would
             * skip the unlock and permanently poison the rwlock (the exact
             * failure this project hit with the old rewind path).  Keeping
             * the frame preserves lock state so the function can finish on
             * resume.  Do NOT reinject the fault either, or the target would
             * die (it has no handler for these). */
            stop_all_threads();
            fprintf(stderr, "Error: %s() faulted with signal %d; "
                    "keeping the target in its current frame\n",
                    g_dbg.call_func_name, sig);
            abort_call_keep_frame();
            return 2;
        }
        /* Benign signal during the call (SIGALRM, SIGCHLD, ...): reinject
         * it exactly like gdb does and keep waiting for the return. */
        DBG("call_wait: reinjecting signal %d", sig);
        ptrace(PTRACE_CONT, g_dbg.target_pid, NULL, (void *)(long)sig);
        /* continue waiting */
    }
}

/* Per-call out-parameter staging.  A local ('<type> <name> = <val>')
 * symbol is backed by a debugger-side heap copy; when a 'call' references
 * it with '&name' the debugger copies the value into a temporary slot on
 * the target stack, hands that address to the callee, then copies the
 * result back into the debugger copy after the call returns.  Nothing is
 * ever allocated on the target heap, so nothing leaks in the target. */
#define MAX_OUTARGS MAX_FUNC_ARGS
typedef struct {
    char          name[MAX_NAME_LEN];
    unsigned long copy_addr;    /* debugger-process heap copy */
    unsigned long target_addr;  /* target-stack staging slot */
    unsigned long size;
    int           arg_index;    /* func_arg_t 槽位（0 起：寄存器位或栈传参位） */
} out_arg_t;

/* 一次 call 的解析结果：参数数组、参数个数与出参登记，
 * 由 parse_call_args 填充，供 cmd_call_function 直接使用。 */
typedef struct {
    func_arg_t fargs[MAX_FUNC_ARGS];
    int        arg_count;
    out_arg_t  outargs[MAX_OUTARGS];
    int        n_outargs;
} call_args_t;

/**
 * @brief 解析字符串参数（"..."，不含引号）
 *
 * @param[in]     ap   当前位置，指向开引号之后的字符
 * @param[in,out] args 调用参数集（填充参数数组并递增计数）
 * @return 下一个待解析位置；NULL 表示引号不匹配
 */
static const char *parse_string_arg(const char *ap, call_args_t *args)
{
    const char *eq = strchr(ap, '"');
    if (eq == NULL) {
        printf("Error: Unmatched quote in argument\n");
        return NULL;
    }
    size_t sl = (size_t)(eq - ap);
    if (sl >= MAX_STR_ARG - 1)
        sl = MAX_STR_ARG - 2;
    func_arg_t *f = &args->fargs[args->arg_count];
    f->type = ARG_STRING;
    memcpy(f->str_val, ap, sl);
    f->str_val[sl] = '\0';
    args->arg_count++;
    return eq + 1;
}

/**
 * @brief 解析 '&' 出参引用参数（&name）
 *
 * 本地 scratch 符号（'<type> <name> = <val>' 创建）登记到 outargs，
 * 调用返回后在目标栈上暂存并回写；目标/.so 符号直接取其地址作参数值。
 *
 * @param[in]     ap   当前位置，指向 '&'
 * @param[in,out] args 调用参数集（含出参登记数组）
 * @return 下一个待解析位置；NULL 表示符号缺失或出参数目超限
 */
static const char *parse_ref_arg(const char *ap, call_args_t *args)
{
    char ref_name[MAX_NAME_LEN] = {0};
    const char *q = ap + 1;
    size_t rl = 0;
    while (*q != '\0' && !isspace((unsigned char)*q) &&
           *q != ',' && *q != ')') {
        if (rl < MAX_NAME_LEN - 1)
            ref_name[rl++] = *q;
        q++;
    }
    symbol_t *rs = find_symbol_any(ref_name);
    if (rs == NULL) {
        printf("Error: symbol '%s' not found for '&' reference\n", ref_name);
        return NULL;
    }
    func_arg_t *f = &args->fargs[args->arg_count];
    if (is_local_symbol(rs)) {
        /* 调试器侧出参副本：登记暂存槽，真实目标地址在栈布局时回填 */
        if (args->n_outargs >= MAX_OUTARGS) {
            printf("Error: too many out-parameters (max %d)\n", MAX_OUTARGS);
            return NULL;
        }
        out_arg_t *o = &args->outargs[args->n_outargs];
        safe_strcpy(o->name, ref_name, MAX_NAME_LEN);
        o->copy_addr   = rs->addr;
        o->size        = rs->size;
        o->arg_index   = args->arg_count;
        o->target_addr = 0;   /* 栈布局时回填 */
        args->n_outargs++;
        f->type    = ARG_INT;
        f->int_val = 0;       /* 占位符 */
    } else {
        /* 目标/.so 符号：地址在目标侧有效 */
        f->type    = ARG_INT;
        f->int_val = rs->addr;
    }
    args->arg_count++;
    return q;
}

/**
 * @brief 解析整型字面量或裸符号名参数
 *
 * 可转换为数字的按整型处理；否则视为符号名登记为 ARG_SYMBOL，
 * 待目标停止后取其当前值传入（调试器侧局部与目标/.so 符号均可用）。
 *
 * @param[in]     ap   当前位置
 * @param[in,out] args 调用参数集
 * @return 下一个待解析位置；NULL 表示无效参数
 */
static const char *parse_int_or_sym_arg(const char *ap, call_args_t *args)
{
    char *end;
    unsigned long val = strtoul(ap, &end, 0);
    func_arg_t *f = &args->fargs[args->arg_count];
    if (end != ap) {
        f->type    = ARG_INT;
        f->int_val = val;
        args->arg_count++;
        return end;
    }
    char sym_name[MAX_NAME_LEN] = {0};
    const char *q = ap;
    size_t sl = 0;
    while (*q != '\0' && !isspace((unsigned char)*q) &&
           *q != ',' && *q != ')') {
        if (sl < MAX_NAME_LEN - 1)
            sym_name[sl++] = *q;
        q++;
    }
    symbol_t *vs = find_symbol_any(sym_name);
    if (vs == NULL) {
        printf("Error: Invalid argument '%s'\n", ap);
        return NULL;
    }
    f->type     = ARG_SYMBOL;
    f->int_val  = 0;          /* 占位符 */
    f->sym_addr = vs->addr;
    f->sym_size = vs->size;
    args->arg_count++;
    return q;
}

/**
 * @brief 解析 call 命令的实参列表（括号内内容）
 *
 * 支持四种参数形式：字符串、"&"出参引用、整型字面量、裸符号名。
 * 出参引用指向本地 scratch 符号时登记到 outargs，供后续栈上暂存。
 *
 * @param[in]     lparen 左括号位置，指向 '('
 * @param[in]     rparen 右括号位置，指向 ')'
 * @param[in,out] args   解析结果：参数数组、参数个数与出参登记
 * @return 0 成功，-1 失败（语法错误/符号缺失等）
 */
static int parse_call_args(const char *lparen, const char *rparen,
                           call_args_t *args)
{
    if (lparen + 1 >= rparen)
        return 0;   /* 空括号：无参数 */

    size_t alen = (size_t)(rparen - lparen - 1);
    char *abuf = malloc(alen + 1);
    if (abuf == NULL)
        return -1;
    memcpy(abuf, lparen + 1, alen);
    abuf[alen] = '\0';

    const char *ap = abuf;
    while (*ap != '\0' && args->arg_count < MAX_FUNC_ARGS) {
        while (*ap != '\0' && isspace((unsigned char)*ap))
            ap++;
        if (*ap == '\0')
            break;

        const char *next = NULL;
        if (*ap == '"')
            next = parse_string_arg(ap + 1, args);
        else if (*ap == '&')
            next = parse_ref_arg(ap, args);
        else
            next = parse_int_or_sym_arg(ap, args);
        if (next == NULL) {
            free(abuf);
            return -1;
        }
        ap = next;
        while (*ap != '\0' && isspace((unsigned char)*ap))
            ap++;
        if (*ap == ',')
            ap++;
    }
    /* 参数超上限：显式报错而非静默丢弃。历史缺陷：参数数组只按寄存器
     * 参数数开（ARM32=4）时第 5 参被无声忽略；现 ARM32 上限提至 8
     * （r0..r3 + 第 5..8 参栈传参），仍超出（如 9 个）在此报错。
     * aarch64=8 / x86=6 同走此路径。 */
    while (*ap != '\0' && isspace((unsigned char)*ap))
        ap++;
    if (*ap != '\0') {
        printf("Error: too many arguments (max %d for this architecture)\n",
               MAX_FUNC_ARGS);
        free(abuf);
        return -1;
    }
    free(abuf);
    return 0;
}

/**
 * @brief 向目标注入一次函数调用并同步执行：call <func>(args)
 *
 * 解析函数名与参数（整型/字符串/&符号引用/裸符号值），停止全部线程，
 * 在 .text NOP 填充区放置返回断点，改写 PC/SP/LR/参数寄存器后放行，
 * 同步等待返回断点命中，收取返回值并恢复调用前状态，最后自动 resume。
 *
 * @param[in] func_spec 用户输入的命令参数，格式 "func(arg1,arg2,...)"
 * @return 0 成功，-1 失败
 */
int cmd_call_function(const char *func_spec)
{
    if (!g_dbg.is_attached) {
        printf("Error: Not attached.\n"); return -1;
    }
    if (g_dbg.call_in_progress) {
        printf("Error: another function call is already in progress\n");
        return -1;
    }
    if (g_dbg.call_sync_done) {
        /* The previous call left the target stopped with state restored.
         * Running a new call while the target is stopped on the interrupted
         * instruction would lose that stop; resume first (gdb semantics). */
        printf("Error: target is stopped after a previous call; "
               "type 'c' to continue it first.\n");
        return -1;
    }

    /* ---- Parse function name ---- */
    const char *lparen = strchr(func_spec, '(');
    const char *rparen = strrchr(func_spec, ')');
    if (!lparen || !rparen || rparen < lparen) {
        printf("Error: Syntax: call func_name(arg1, arg2, ...)\n");
        return -1;
    }

    char func_name[MAX_NAME_LEN] = {0};
    size_t nlen = (size_t)(lparen - func_spec);
    if (nlen >= MAX_NAME_LEN) nlen = MAX_NAME_LEN - 1;
    memcpy(func_name, func_spec, nlen);
    func_name[nlen] = '\0';

    /* Trim trailing whitespace from function name */
    char *tn = func_name + strlen(func_name) - 1;
    while (tn >= func_name && isspace((unsigned char)*tn)) *tn-- = '\0';
    char *sn = func_name;
    while (*sn && isspace((unsigned char)*sn)) sn++;
    if (sn != func_name) memmove(func_name, sn, strlen(sn) + 1);

    /* ---- Parse arguments into func_arg_t array ---- */
    call_args_t args;
    memset(&args, 0, sizeof(args));
    if (parse_call_args(lparen, rparen, &args) != 0)
        return -1;
    func_arg_t *fargs = args.fargs;
    out_arg_t *outargs = args.outargs;
    int arg_count = args.arg_count;
    int n_outargs = args.n_outargs;

    /* ---- Look up function symbol ---- */
    symbol_t *sym = find_symbol(func_name, SYM_FUNC);
    if (!sym) {
        sym = find_symbol_any(func_name);
        if (sym && sym->type != SYM_FUNC) {
            printf("Error: '%s' is not a function\n", func_name);
        } else {
            printf("Error: Function '%s' not found\n", func_name);
        }
        return -1;
    }

    /* ---- Sibling threads keep running during the injected call ----
     * This mirrors gdb's proceed(): only the leader is stopped to rewrite
     * its registers; every sibling stays RUNNING so that a lock the callee
     * needs (e.g. list_rwlock in write_data_to_tag_by_name) can still be
     * acquired/released by its owner.  Freezing siblings wedged the callee
     * on a lock held by an INTERRUPT-stopped thread and, after the timeout
     * abort rewound the PC, left that lock permanently polluted — every
     * later call then timed out.  We freeze everything only when the
     * return-breakpoint fires (or on abort), like gdb's stop_all_threads(). */
    if (ptrace_stop_target(g_dbg.target_pid) != 0) return -1;

    arch_regs_t orig_regs, new_regs;
    if (arch_get_regs(g_dbg.target_pid, &orig_regs) != 0) {
        ptrace_continue_target(g_dbg.target_pid); return -1;
    }
    /* Save a pristine copy BEFORE any modifications */
    memcpy(&g_dbg.saved_regs, &orig_regs, sizeof(orig_regs));
    memcpy(&new_regs, &orig_regs, sizeof(orig_regs));

    /* ---- Place the return-breakpoint in .text NOP padding ----
     * gdb's create_call_dummy() never parks the return trap in live code.
     * The old "BRK at the interrupted PC" scheme was fatal when that PC
     * sat in a shared library: sibling threads re-executing that very
     * instruction hit our trap and the kernel killed the whole process
     * with SIGTRAP (status 0x85).  We instead overwrite one NOP of the
     * executable's .text alignment padding — dead code that no thread
     * ever executes — with the BRK.  The slot is resolved fresh on every
     * call (call_find_nop_slot) and does not depend on the current PC. */
    unsigned long bp_addr = call_find_nop_slot(g_dbg.target_pid);
    g_dbg.nop_slot_addr = bp_addr;
    if (bp_addr == 0) {
        /* NOTE: never fall back to the current PC here — in a multithreaded
         * target that address is live code and may be re-executed by sibling
         * threads, which would hit our trap and cause unpredictable behavior
         * (even SIGTRAP-killing the whole process).  Without a safe NOP
         * slot there is no home for the return-breakpoint: clean up and
         * abort the call. */
        fprintf(stderr,
                "Error: no NOP padding found for the return-breakpoint; "
                "'call' aborted\n");
        ptrace_continue_target(g_dbg.target_pid);
        return -1;
    }
    DBG("call: func_addr=0x%lx  bp_addr=0x%lx  sp=0x%llx  pc=0x%llx",
        sym->addr, bp_addr,
        (unsigned long long)arch_reg_sp(&orig_regs),
        (unsigned long long)arch_reg_pc(&orig_regs));

    /* ---- Overwrite one NOP of the padding with BRK #0 ---- */
    unsigned char orig_bytes[8] = {0};
    size_t bp_len = 0;
    if (call_write_break(g_dbg.target_pid, bp_addr, orig_bytes, &bp_len) != 0) {
        fprintf(stderr, "Error: failed to write breakpoint at 0x%lx\n", bp_addr);
        ptrace_continue_target(g_dbg.target_pid);
        return -1;
    }
    memcpy(g_dbg.breakpoint_orig, orig_bytes, sizeof(orig_bytes));
    g_dbg.breakpoint_len = (int)bp_len;
    g_dbg.breakpoint_addr = bp_addr;   /* set NOW so error paths can restore */
    DBG("call: breakpoint written at 0x%lx, %d byte(s)", bp_addr, (int)bp_len);

    /* ---- Resolve bare-symbol arguments ----
     * The target is stopped now, so ptrace reads of target/so symbols work,
     * and debugger-side locals are read straight from the heap copy.
     * The value (little-endian low bytes of the symbol) is passed as an
     * integer argument.  All slots are resolved — including ARM32's 5th..8th
     * stacked arguments, which may also be bare symbols. */
    for (int i = 0; i < arg_count; i++) {
        if (fargs[i].type == ARG_SYMBOL) {
            unsigned long val = 0;
            size_t n = (fargs[i].sym_size < sizeof(val))
                       ? fargs[i].sym_size : sizeof(val);
            if (read_any_mem(fargs[i].sym_addr, &val, n) == 0) {
                fargs[i].int_val = val;
                if (g_dbg.verbose)
                    printf("  arg[%d] symbol value: 0x%lx (%lu)\n", i, val, val);
            } else {
                printf("Error: failed to read symbol value at 0x%lx\n",
                       fargs[i].sym_addr);
                ptrace_continue_target(g_dbg.target_pid);
                return -1;
            }
        }
    }

    /* ---- Prepare argument values: register words + AAPCS stack-arg area ----
     * 每个参数折叠为单 word（整数/符号值，或字符串/出参的暂存地址）。
     * 第 1..ARCH_MAX_INT_ARGS 个走寄存器；ARM32（AAPCS32）第 5..8 参须压入
     * "栈参数区"——区起点即注入后的入口 SP，第 5 参落在 [SP,#0]，按 4 字节
     * 递增（区总长按 ARCH_STACK_ALIGN 上取整，保证入口 SP 8 字节对齐），
     * 与编译器生成的 caller 布局完全一致。aarch64/x86 不启用该区
     * （ARCH_MAX_CALL_ARGS == ARCH_MAX_INT_ARGS，超限已在 parse_call_args
     * 报错）；x86 call 注入机制本身仍未实现（无 LR）。所有暂存区置于最终
     * SP 之上被调函数参数区之外的安全区，callee 序言只向 SP 之下建帧，
     * 不会触碰。 */
    unsigned long arg_word[MAX_FUNC_ARGS] = {0};
    char is_out[MAX_FUNC_ARGS] = {0};  /* 出参位：地址由 out-param staging 回填 */

    {
        unsigned long long stack_top =
            arch_reg_sp(&orig_regs) & ~(unsigned long long)(ARCH_STACK_ALIGN - 1);
        /* 1) Stage debugger-side out-param copies on the target stack
         *    (above the final SP, aligned) and hand their addresses to the
         *    callee.  The target is stopped here, so mem_write is safe.  The
         *    staging slots remain readable after the callee returns because
         *    nothing overwrites them before we read them back. */
        for (int i = 0; i < n_outargs; i++) {
            size_t aligned = (outargs[i].size + ARCH_STACK_ALIGN - 1)
                             & ~(unsigned long)(ARCH_STACK_ALIGN - 1);
            stack_top -= aligned;
            outargs[i].target_addr = (unsigned long)stack_top;
            if (mem_write(g_dbg.target_pid, stack_top,
                          (const void *)outargs[i].copy_addr,
                          outargs[i].size) != 0) {
                fprintf(stderr, "Error: failed to stage out-param '%s' "
                        "@ 0x%lx on target stack\n",
                        outargs[i].name, outargs[i].target_addr);
                call_restore_break();
                ptrace_continue_target(g_dbg.target_pid); return -1;
            }
            arg_word[outargs[i].arg_index] = outargs[i].target_addr;
            is_out[outargs[i].arg_index]   = 1;
            DBG("call: out-param '%s' staged @ 0x%lx (%luB)",
                outargs[i].name, outargs[i].target_addr, outargs[i].size);
        }
        /* 2) string args above the out-params; every non-out slot folds to
         *    its int value.  Runs over ALL args: a string / int placed at
         *    the 5th..8th position is a stacked argument, not a register
         *    one, but still needs its bytes staged here. */
        for (int i = 0; i < arg_count; i++) {
            if (fargs[i].type == ARG_STRING) {
                size_t sl = strlen(fargs[i].str_val);
                size_t aligned = (sl + 1 + ARCH_STACK_ALIGN - 1)
                                 & ~(unsigned long)(ARCH_STACK_ALIGN - 1);
                stack_top -= aligned;
                if (mem_write(g_dbg.target_pid, stack_top,
                              fargs[i].str_val, sl + 1) != 0) {
                    call_restore_break();
                    ptrace_continue_target(g_dbg.target_pid); return -1;
                }
                arg_word[i] = (unsigned long)stack_top;   /* pointer to string */
            } else if (!is_out[i]) {
                arg_word[i] = fargs[i].int_val;
            }
        }
#ifdef ARCH_ARM32
        /* 3) AAPCS32 stacked-argument area (args 5..8).  The area is carved
         *    at the LOWEST address we allocate and its base becomes the new
         *    entry SP, so the callee reads arg#5 from [SP,#0], arg#6 from
         *    [SP,#4], ... exactly as a compiler-generated caller would lay
         *    them out.  The string/out staging above stays clear of the
         *    area, in caller-frame space the callee never touches. */
        {
            int stack_args = arg_count - ARCH_MAX_INT_ARGS;
            if (stack_args > 0) {
                size_t area = (size_t)stack_args * sizeof(unsigned long);
                area = (area + ARCH_STACK_ALIGN - 1)
                       & ~(size_t)(ARCH_STACK_ALIGN - 1);
                stack_top -= area;
                for (int k = 0; k < stack_args; k++) {
                    unsigned long w = arg_word[ARCH_MAX_INT_ARGS + k];
                    if (mem_write(g_dbg.target_pid,
                                  stack_top +
                                      (unsigned long long)(k * sizeof(unsigned long)),
                                  &w, sizeof(w)) != 0) {
                        fprintf(stderr, "Error: failed to write stacked arg[%d] "
                                "at 0x%llx\n",
                                ARCH_MAX_INT_ARGS + k,
                                (unsigned long long)stack_top);
                        call_restore_break();
                        ptrace_continue_target(g_dbg.target_pid); return -1;
                    }
                }
            }
        }
#endif /* ARCH_ARM32 */
        arch_reg_set_sp(&new_regs, stack_top);   /* 保持对齐 */
        arch_reg_set_lr(&new_regs, arch_ret_addr(bp_addr)); /* 返回地址=断点槽位（Thumb 带 bit0） */
        arch_reg_set_pc(&new_regs, sym->addr);   /* PC = 函数入口 */
        for (int i = 0; i < arg_count && i < ARCH_MAX_INT_ARGS; i++)
            arch_reg_set_arg(&new_regs, i, arg_word[i]);
        DBG("call: new pc=0x%lx sp=0x%llx lr=0x%lx narg=%d",
            sym->addr, (unsigned long long)stack_top, bp_addr, arg_count);
    }

    /* ---- Apply registers and launch the call ---- */
    if (arch_set_regs(g_dbg.target_pid, &new_regs) != 0) {
        ptrace_continue_target(g_dbg.target_pid); return -1;
        call_restore_break();
    }

    safe_strcpy(g_dbg.call_func_name, func_name, MAX_NAME_LEN);
    g_dbg.call_has_retval = 0;
    g_dbg.call_return_value = 0;
    g_dbg.call_in_progress = 1;
    g_dbg.call_sync_done = 0;

    if (ptrace_continue_target(g_dbg.target_pid) != 0) {
        abort_call_and_restore();   /* undo injected regs + breakpoint */
        return -1;
    }

    /* ---- Synchronously wait for the return-breakpoint (gdb semantics) ----
     * We never return to the REPL while the call is in flight, so the
     * result is known when this command finishes and the target can never
     * be left wedged on the breakpoint or resumed mid-call. */
    int rc = call_wait_for_return();
    if (rc == 1) {
        printf("  > returned: %lld (0x%llx)\n",
               (long long)g_dbg.call_return_value,
               (unsigned long long)g_dbg.call_return_value);
        fflush(stdout);
        /* Read back every staged out-param into its debugger-side copy.
         * The target is still stopped on the return-breakpoint, so the
         * callee's writes to the staging slots are fully visible. */
        for (int i = 0; i < n_outargs; i++) {
            if (mem_read(g_dbg.target_pid, outargs[i].target_addr,
                         (void *)outargs[i].copy_addr,
                         outargs[i].size) != 0) {
                printf("Warning: failed to read back out-param '%s'\n",
                       outargs[i].name);
            } else {
                DBG("call: out-param '%s' read back %luB from 0x%lx",
                    outargs[i].name, outargs[i].size, outargs[i].target_addr);
            }
        }
        /* The call finished cleanly: resume the target automatically so
         * the user does not have to type 'c' manually. */
        if (ptrace_continue_target(g_dbg.target_pid) != 0)
            return -1;
        resume_all_threads();
        g_dbg.call_sync_done = 0;
        return 0;
    }
    if (rc == 2 || rc == 3) {
        if (g_dbg.call_stopped_in_frame) {
            /* gdb `unwind-on-timeout off' / `unwind-on-signal off' (both
             * default) semantics: the target is still stopped INSIDE the
             * callee with its PC unrewound — reached on Ctrl-C interrupt,
             * unexpected SIGTRAP, PTRACE_EVENT_STOP or a fatal fault in the
             * callee.  Any lock it holds stays consistent with the execution
             * flow.  We do NOT auto-resume — the user must decide between
             * letting the function finish and restarting the target. */
            arch_regs_t cur;
            if (arch_get_regs(g_dbg.target_pid, &cur) == 0) {
                fprintf(stderr, "  target pc=0x%llx: ",
                        (unsigned long long)arch_reg_pc(&cur));
                cmd_lookup_symbol_by_addr((unsigned long)arch_reg_pc(&cur));
            }
            printf("  Call aborted; target is stopped INSIDE %s() "
                   "(frame kept, no PC rewind).\n"
                   "  Lock state is intact.  Options:\n"
                   "    - 'c'      : continue (risky: the function may return\n"
                   "                 into the restored NOP slot)\n"
                   "    - restart  : recommended if %s() is wedged\n",
                   g_dbg.call_func_name, g_dbg.call_func_name);
            fflush(stdout);
            /* Target stays stopped; REPL prompt shows (stopped); 'c' resumes. */
            return -1;
        }
        printf("  Call aborted; pre-call state restored.\n");
        fflush(stdout);
        /* Defensive: today every rc==2/3 path keeps the frame, so this
         * restore branch is unreachable.  Kept in case a future path
         * rewinds again. */
        if (ptrace_continue_target(g_dbg.target_pid) != 0)
            return -1;
        resume_all_threads();
        g_dbg.call_sync_done = 0;
        return -1;
    }
    return -1;   /* rc == -1: target exited/killed (nothing to resume) */
}
