技术知识库

将 C 程序分阶段交叉编译并运行在 ARM64

将 C 程序分阶段交叉编译并运行在 ARM64

摘要

本实验在 Ubuntu 26.04 x86-64 宿主机上,用原生 GCC 建立运行基准,再用 aarch64-linux-gnu-gcc 显式完成预处理、编译、汇编和静态链接。最终 ELF 经 SSH 复制到自行构建的 ARM64 Debian QEMU Guest,成功输出字符串、分配 409,600 字节并返回状态 0。

原理见 GCC 从源文件到 ELF 的四个阶段。

环境与路径

宿主机:Ubuntu 26.04,x86-64
目标机:QEMU ARM64,Linux 5.0.0+,Debian 12
工作目录:/home/martin-zou/workspace/gcc-lab
交叉编译器:aarch64-linux-gnu-gcc 15.2.0
SSH 转发:127.0.0.1:2222 → ARM64 Guest:22

1. 创建测试程序

mkdir -p /home/martin-zou/workspace/gcc-lab
cd /home/martin-zou/workspace/gcc-lab
vim test.c

写入:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define PAGE_SIZE 4096
#define MAX_SIZE (100 * PAGE_SIZE)

int main(void)
{
    char *buf;

    puts("hello martin.zou");
    buf = malloc(MAX_SIZE);
    if (buf == NULL) {
        puts("malloc error");
        return EXIT_FAILURE;
    }

    memset(buf, 0, MAX_SIZE);
    printf("buffer address=%p, size=%d bytes\n",
           (void *)buf, MAX_SIZE);

    free(buf);
    return EXIT_SUCCESS;
}

2. 原生编译作为基准

gcc \
  -Wall \
  -Wextra \
  -O0 \
  -g \
  test.c \
  -o test-native

file test-native
./test-native
echo "status=$?"

本轮 file 识别为 x86-64 PIE、动态链接、包含调试信息。多次运行均成功,虚拟地址因 ASLR 而变化。

3. ARM64 预处理

aarch64-linux-gnu-gcc \
  -E test.c \
  -o test.i

检查宏和主函数:

ls -lh test.c test.i
grep -n 'int main' test.i
grep -n 'hello martin.zou' test.i

本轮 test.i 为 65,438 字节,明显大于源文件,因为标准库头文件声明已经展开。

4. 编译为 AArch64 汇编

aarch64-linux-gnu-gcc \
  -S test.i \
  -o test.s

检查:

sed -n '1,180p' test.s

本轮 test.s 为 1,052 字节,包含:

.arch armv8-a
stp x29, x30, [sp, -32]!
bl puts
bl malloc
bl memset
bl printf
bl free
ret

这些是 AArch64 指令与寄存器,证明目标架构已经在编译阶段确定。

5. 汇编为可重定位目标文件

aarch64-linux-gnu-gcc \
  -c test.s \
  -o test.o

验证:

ls -lh test.o
file test.o
aarch64-linux-gnu-readelf -h test.o
aarch64-linux-gnu-nm test.o

本轮 test.o 为 2,112 字节。它包含机器码,但 libc 外部符号尚待链接解析。

6. 静态链接 ARM64 ELF

aarch64-linux-gnu-gcc \
  test.o \
  -o test \
  --static

验证:

file test
file test-native
ls -lh test test-native

本轮结果:

test:ARM aarch64,静态链接,面向 GNU/Linux 3.7.0 及以上
test-native:x86-64 PIE,动态链接,解释器 /lib64/ld-linux-x86-64.so.2
test 大小:约 689 KiB

静态链接使该程序不依赖 Guest 中与交叉工具链完全匹配的动态加载器和 libc。

7. 复制到 ARM64 Debian

先确保 ARM64 QEMU 使用如下转发:

hostfwd=tcp::2222-:22

宿主机执行:

scp \
  -P 2222 \
  test \
  martin@127.0.0.1:~/

scp 使用大写 -P 指定端口;ssh 使用小写 -p。

8. 在 ARM64 Guest 运行

ssh -p 2222 martin@127.0.0.1

file /home/martin/test
/home/martin/test
echo "status=$?"

本轮两次实际输出:

hello martin.zou
buffer address=0xffff9ba2a010, size=409600 bytes

hello martin.zou
buffer address=0xa876430, size=409600 bytes

地址变化来自进程虚拟地址布局和内存分配策略,不影响实验成功。关键证据是 ARM64 ELF 能在 Guest 中执行、分配并清零 409,600 字节、正常释放并退出。

9. 待完成:Makefile

书中实验还要求编写一个简单 Makefile,把编译和清理操作自动化。完成该步骤并验证增量构建与 make clean 后,将本 Project 状态更新为 evergreen。

完成标准

  • 保留 .i、.s、.o 和最终 ELF。
  • 能解释四阶段各自产物和职责。
  • file 确认原生程序为 x86-64、交叉程序为 AArch64。
  • ARM64 静态 ELF 在 Debian Guest 中运行成功。
  • Makefile 能完成构建、增量判断和清理。

关联