ARM64平台复制C函数至栈内存执行触发非法指令的问题排查
嘿,你遇到的这个问题其实是ARM64平台的几个特性加上代码里的小疏忽共同导致的,咱们一步步来拆解和解决:
1. 核心问题:函数长度计算完全错误
你声明的__start_myfnsection和__stop_myfnsection是uint64_t类型的数组,所以两者相减得到的是64位元素的个数,而不是函数的字节长度。你输出的9是元素数量,实际函数的字节长度应该是9*8=72字节!
复制长度不够的话,函数指令会被截断,执行到一半就会遇到非法指令,这就是你看到SIGILL的直接原因。
修复方法:把长度计算改成基于地址值的字节数计算,用uintptr_t强制转换地址后相减:
#include <stdint.h> // ... size_t myfn_length = (uintptr_t)__stop_myfnsection - (uintptr_t)__start_myfnsection;
2. ARM64强制要求指令地址4字节对齐
ARM64架构的AArch64状态下,所有执行的指令都是32位(4字节),因此指令地址必须是4字节对齐,否则会直接触发SIGILL非法指令信号。
你用__builtin_alloca分配的栈内存,虽然通常栈本身是16字节对齐的,但分配后的起始地址不一定保证4字节对齐(极端情况下可能出现不对齐的情况),所以需要手动调整:
修复方法:手动对齐内存地址:
// 先分配足够的内存(加上3字节的对齐余量) void* memory = __builtin_alloca(myfn_length + 3); // 调整到最近的4字节对齐地址 memory = (void*)(((uintptr_t)memory + 3) & ~3UL);
3. PIE编译带来的PC相对寻址问题
你用了-pie编译选项,生成的是位置无关可执行文件(PIE)。你的函数myfn里访问了static变量i,这个访问是通过PC相对偏移实现的——也就是基于当前指令的PC地址计算变量的位置。
当你把函数复制到栈上后,函数的PC地址和原来的.text段位置完全不同,原来的偏移就会指向错误的内存地址,后续访问i的时候必然会崩溃(甚至可能在执行到那条指令时出错)。
解决方案:
- 测试阶段可以暂时去掉
-pie编译选项,生成非位置无关的可执行文件,这样访问static变量用的是绝对地址,复制后仍然有效(注意:现代系统可能默认开启PIE,需要显式加-no-pie)。 - 如果必须用PIE,那需要手动修改函数中的寻址逻辑,把static变量也复制到栈上并调整偏移,这会非常复杂,不建议用于简单测试。
4. 验证栈的执行权限
虽然你加了-z execstack,但可以用readelf工具验证栈段是否真的有执行权限:
$ readelf -l ./a.out | grep GNU_STACK
输出中如果GNU_STACK段的Flags包含RWE(Read-Write-Execute),说明权限设置正确;如果只有RW,说明编译选项没生效,需要检查gcc版本或系统配置。
修复后的完整代码示例
把上面的修复点整合后,代码应该是这样的:
#include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> extern uint64_t __start_myfnsection[]; extern uint64_t __stop_myfnsection[]; __attribute__((noinline, section("myfnsection"))) void myfn() { static int i; i++; printf("I love lemons!^^.\n"); printf("i=%d\n", i); return; } int main () { myfn(); // 正确计算函数字节长度 size_t myfn_length = (uintptr_t)__stop_myfnsection - (uintptr_t)__start_myfnsection; printf("Length of myfn() function is: %zu\n", myfn_length); // 分配栈内存并对齐到4字节 void* memory = __builtin_alloca(myfn_length + 3); memory = (void*)(((uintptr_t)memory + 3) & ~3UL); memcpy(memory, __start_myfnsection, myfn_length); void (*myfn_copy)() = memory; myfn_copy(); return 0; }
编译命令(去掉PIE用于测试):
$ gcc -no-pie -z execstack -fno-stack-protector -g test.c
运行后应该能正常输出两次函数内容,i的值分别为1和2,不会再触发非法指令。
备注:内容来源于stack exchange,提问作者Elisa K. K.

