Skip to content

Cross compile

Embedded Linux hầu như luôn phát triển trên máy host (x86-64, mạnh, đủ công cụ) nhưng chạy trên máy target (ARM, RISC-V, MIPS...). Cross compile là build trên host ra executable cho target. Quy trình 4 bước ở bài 1 không đổi; thứ đổi là mọi thứ nó dùng: compiler, assembler, linker, header, và library đều phải thuộc kiến trúc target.

        host (x86-64)                              target (ARM)
┌──────────────────────────────┐              ┌──────────────────────┐
│ arm-linux-gnueabihf-gcc, ld  │              │                      │
│ sysroot: header + libc.so    │ ──build──▶   │  ./hello  (ARM ELF)  │
│          của ARM             │   copy       │  libc.so (ARM)       │
└──────────────────────────────┘              └──────────────────────┘

Ví dụ trong bài dùng arm-linux-gnueabihf-gcc (ARM 32-bit) và aarch64-linux-gnu-gcc (ARM 64-bit) bản 13.3 trên Ubuntu, cài bằng apt install gcc-arm-linux-gnueabihf gcc-aarch64-linux-gnu. Ngoài ra còn có thể dùng toolchain từ Yocto, Buildroot, Arm GNU Toolchain hay Bootlin.

Ba khái niệm: build, host, target

Tài liệu của GNU và Autotools phân biệt ba máy:

Máy Là gì Ví dụ khi build một cross compiler Khi build ứng dụng
build máy đang chạy quá trình build x86-64 x86-64
host máy sẽ chạy sản phẩm x86-64 ARM
target máy mà sản phẩm sinh code cho ARM (không dùng)

Trong đời sống embedded, người ta thường gọi PC phát triển là "host" và board là "target". GNU Autotools dùng nghĩa khác: host là máy chạy sản phẩm. Vì vậy cross-build một ứng dụng bằng Autotools phải viết ./configure --host=arm-linux-gnueabihf (host = board ARM). Cần để ý khi đọc tài liệu.

Tên toolchain: target triplet

Tiền tố của mọi công cụ trong cross toolchain là triplet (thực ra thường có 3-4 phần):

arm - linux - gnueabihf
 │      │        └── ABI + libc: GNU libc, EABI, hard-float
 │      └─────────── OS: linux  (bare-metal dùng "none")
 └────────────────── CPU architecture
$ arm-linux-gnueabihf-gcc -dumpmachine
arm-linux-gnueabihf
$ aarch64-linux-gnu-gcc -dumpmachine
aarch64-linux-gnu
Triplet Ý nghĩa
arm-linux-gnueabihf ARM 32-bit, Linux, glibc, hard-float ABI
arm-linux-gnueabi ARM 32-bit, Linux, glibc, soft-float ABI
aarch64-linux-gnu ARM 64-bit, Linux, glibc
arm-none-eabi ARM bare-metal (Cortex-M, firmware), thường newlib
arm-linux-musleabihf giống gnueabihf nhưng libc là musl
riscv64-linux-gnu RISC-V 64-bit, Linux, glibc

Trong thực tế bạn sẽ thấy CROSS_COMPILE=arm-linux-gnueabihf- (có dấu - ở cuối) ở Linux kernel, U-Boot và Buildroot: build system tự ghép với gcc, ld, objcopy, strip...

$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage

Chứng minh: executable là của target

$ arm-linux-gnueabihf-gcc hello.c math.c -o h_arm
$ file h_arm
h_arm: ELF 32-bit LSB pie executable, ARM, EABI5 version 1 (SYSV), dynamically linked,
       interpreter /lib/ld-linux-armhf.so.3, ...

$ aarch64-linux-gnu-gcc hello.c math.c -o h_a64
$ file h_a64
h_a64: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked,
       interpreter /lib/ld-linux-aarch64.so.1, ...

So với x86-64 ở bài 4, có hai điểm khác: cùng source nhưng architecture khác (ARM, aarch64) và interpreter khác (/lib/ld-linux-armhf.so.3). Target phải có đúng loader và libc này.

Chạy trực tiếp trên host sẽ lỗi:

$ ./h_arm
bash: ./h_arm: cannot execute binary file: Exec format error

Nếu muốn thử nhanh mà chưa có board, dùng qemu-user để chạy executable ARM trên host. Cần chỉ cho nó nơi có libc của target bằng -L:

$ qemu-arm -L /usr/arm-linux-gnueabihf ./h_arm
area = 12.566360
$ qemu-aarch64 -L /usr/aarch64-linux-gnu ./h_a64
area = 12.566360
$ qemu-arm ./h_arm
qemu-arm: Could not open '/lib/ld-linux-armhf.so.3': No such file or directory

Lỗi cuối là tình huống mà target thực sự gặp phải khi thiếu loader: INTERP trỏ tới file không tồn tại.

Sysroot: "gốc" của target trên host

Để link được, linker cần libc.so của ARM, và compiler cần header (stdio.h...) khớp với libc đó, không phải của host. Tất cả được gom trong sysroot: một thư mục có cấu trúc giống / của target.

sysroot/
├── usr/include/      # stdio.h, linux/*.h ... của target
├── usr/lib/          # libc.so, crt1.o ... của target
└── lib/              # ld-linux-armhf.so.3 ...

Xem compiler đang tìm ở đâu:

$ arm-linux-gnueabihf-gcc -E -v - </dev/null 2>&1 | sed -n '/#include <...>/,/End of/p'
 /usr/lib/gcc-cross/arm-linux-gnueabihf/13/include
 /usr/lib/gcc-cross/arm-linux-gnueabihf/13/../../../../arm-linux-gnueabihf/include
 /usr/include
End of search list.

$ arm-linux-gnueabihf-gcc -print-file-name=libc.so
/usr/lib/gcc-cross/arm-linux-gnueabihf/13/../../../../arm-linux-gnueabihf/lib/libc.so
$ arm-linux-gnueabihf-gcc -print-sysroot
/

Toolchain của Ubuntu cài header/library ARM vào /usr/arm-linux-gnueabihf/, tách riêng với /usr/include của host (-print-sysroot trả /, nghĩa là không dùng sysroot riêng). Toolchain từ Yocto/Buildroot/Bootlin thì khác: đóng gói một sysroot riêng, và dùng --sysroot:

$ cp -a /usr/arm-linux-gnueabihf sr/        # mô phỏng một sysroot
$ arm-linux-gnueabihf-gcc --sysroot=$PWD/sr/arm-linux-gnueabihf hello.c math.c -o hsr
$ file hsr
hsr: ELF 32-bit LSB pie executable, ARM, EABI5 version 1 (SYSV), dynamically linked, ...

Khi --sysroot=DIR được đặt, mọi đường dẫn tuyệt đối kiểu /usr/include hay /usr/lib đều được hiểu là DIR/usr/include, DIR/usr/lib. Lỗi kinh điển khi cross compile là sysroot không khớp với image chạy trên target (libc khác phiên bản), dẫn đến lỗi version 'GLIBC_2.xx' not found lúc chạy. Vì vậy sysroot nên sinh ra từ cùng build với rootfs của target; Yocto SDK làm điều này tự động.

Đừng link nhầm library của host

Lỗi hay gặp: dùng -I/usr/include hoặc -L/usr/lib (đường dẫn của host) trong Makefile hay pkg-config của project. Compiler ARM sẽ đọc header x86 hoặc linker báo file in wrong format. Dấu hiệu nhận biết: thông báo skipping incompatible /usr/lib/x86_64-linux-gnu/libfoo.so. Với pkg-config, đặt PKG_CONFIG_SYSROOT_DIR và PKG_CONFIG_LIBDIR trỏ vào sysroot.

CPU, FPU và ABI: các option kiến trúc

Cùng một triplet arm-linux-gnueabihf có thể sinh code cho nhiều CPU khác nhau. Mặc định lấy từ cấu hình lúc toolchain được build:

$ arm-linux-gnueabihf-gcc -v 2>&1 | grep -oE "\-\-with-(arch|float|mode)=[^ ]*"
--with-arch=armv7-a+fp
--with-float=hard
--with-mode=thumb

Nghĩa là mặc định: ARMv7-A với VFP, hard-float, Thumb-2. Muốn tối ưu cho CPU cụ thể (ví dụ Cortex-A9 có NEON như Zynq-7000 hay i.MX6):

$ arm-linux-gnueabihf-gcc -mcpu=cortex-a9 -mfpu=neon -O2 -c hello.c -o ca9.o
$ arm-linux-gnueabihf-readelf -A ca9.o | grep -E "CPU_name|FP_arch|Advanced_SIMD"
  Tag_CPU_name: "7-A"
  Tag_FP_arch: VFPv3
  Tag_Advanced_SIMD_arch: NEONv1
Option Chọn Ví dụ
-march= tập lệnh (ISA) armv7-a, armv8-a, armv5te
-mcpu= CPU cụ thể (ngầm định -march + tuning) cortex-a9, cortex-a53
-mfpu= đơn vị floating-point/SIMD (ARM 32-bit) vfpv3-d16, neon
-mfloat-abi= cách truyền tham số float hard, softfp, soft
-mthumb / -marm tập lệnh Thumb-2 hay ARM

Hai điều đáng nhớ:

1. Chọn -march cao hơn CPU thật sẽ cho executable chạy lỗi Illegal instruction trên board. Nên chọn đúng hoặc thấp hơn CPU của target.

2. -mfloat-abi là một ABI, không chỉ là tối ưu: mọi thứ link cùng nhau (executable, mọi .so, libc) phải cùng float ABI.

-mfloat-abi Tham số/trả về float qua Lệnh float
soft thanh ghi số nguyên gọi hàm thư viện (__aeabi_dmul...)
softfp thanh ghi số nguyên dùng lệnh FPU
hard thanh ghi FPU dùng lệnh FPU

-mfloat-abi được ghi vào object file (Tag_ABI_VFP_args) và readelf -h hiện hard-float ABI trong Flags:

$ arm-linux-gnueabihf-readelf -h h_arm | grep Flags
  Flags:                             0x5000400, Version5 EABI, hard-float ABI

Hàm nhân float hiển thị rõ khác biệt: với hard-float ARMv7 + VFP, một phép x * y + 1.0 biên dịch thành một lệnh FPU duy nhất:

$ arm-linux-gnueabihf-gcc -O2 -S f.c -o -         # double scale(double x, double y) { return x * y + 1.0; }
scale:
    vmov.f64    d7, #1.0e+0
    vmla.f64    d7, d0, d1        @ d7 += d0 * d1; x, y nằm sẵn trong d0, d1
    vmov.f64    d0, d7
    bx  lr

Nếu CPU không có FPU, -march=armv5te -mfloat-abi=hard bị từ chối ngay:

$ arm-linux-gnueabihf-gcc -march=armv5te -mfloat-abi=hard -c hello.c
cc1: error: '-mfloat-abi=hard': selected architecture lacks an FPU

Mặt khác, đổi sang soft trên toolchain gnueabihf không đủ: libc của toolchain này được build với hard-float, nên header và library khác cũng không tồn tại:

$ arm-linux-gnueabihf-gcc -mfloat-abi=soft hello.c math.c
.../gnu/stubs.h:7:11: fatal error: gnu/stubs-soft.h: No such file or directory

Bài học: đổi float ABI nghĩa là đổi cả toolchain và rootfs (gnueabi vs gnueabihf). Với hệ thống Linux hiện đại, hard là lựa chọn phổ biến; soft còn gặp trong core không có FPU.

Thumb và ARM instruction set cũng có thể chọn riêng cho từng file:

$ arm-linux-gnueabihf-gcc -marm   -O2 -c math.c && arm-linux-gnueabihf-objdump -d math.o   # 4 byte/lệnh
   0:   e0000090    mul r0, r0, r0
   4:   e12fff1e    bx  lr
$ arm-linux-gnueabihf-gcc -mthumb -O2 -c math.c && arm-linux-gnueabihf-objdump -d math.o   # 2-4 byte/lệnh
   0:   fb00 f000   mul.w   r0, r0, r0
   4:   4770        bx  lr

Thumb-2 dùng lệnh 16/32-bit nên code nhỏ hơn: lý do toolchain này mặc định --with-mode=thumb.

Static hay dynamic trên target?

Khi chưa có rootfs hoàn chỉnh hoặc không chắc libc khớp, -static là cách nhanh để một binary chạy được:

$ arm-linux-gnueabihf-gcc -static hello.c math.c -o h_arm_s
$ file h_arm_s
h_arm_s: ELF 32-bit LSB executable, ARM, EABI5 version 1 (GNU/Linux), statically linked, ...
$ qemu-arm ./h_arm_s              # chạy được không cần -L
area = 12.566360
$ ls -l h_arm h_arm_s
-rwxr-xr-x 1 root root   7876 h_arm
-rwxr-xr-x 1 root root 423704 h_arm_s

Không còn INTERP nên không cần loader hay libc.so trên target, đổi lại binary lớn hơn ~50 lần (so sánh trong bài 3).

Tích hợp với build system

Makefile

Dùng biến CC hoặc CROSS_COMPILE:

CROSS_COMPILE ?=
CC := $(CROSS_COMPILE)gcc
STRIP := $(CROSS_COMPILE)strip
$ make CROSS_COMPILE=arm-linux-gnueabihf-

CMake: toolchain file

Toolchain file khai báo tất cả những gì liên quan tới target ở một chỗ, tách khỏi CMakeLists.txt:

arm.cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
# set(CMAKE_SYSROOT /path/to/sysroot)

# Tìm program ở host, nhưng library/header chỉ ở sysroot
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
$ cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=arm.cmake
$ cmake --build build
$ file build/app
build/app: ELF 32-bit LSB pie executable, ARM, EABI5 version 1 (SYSV), ...

Ba dòng FIND_ROOT_PATH_MODE_* là điểm quan trọng nhất: chúng làm find_library/find_package không vô tình tìm thấy library x86 của host. Xem thêm CMake.

Yocto SDK

Với Yocto, bitbake <image> -c populate_sdk tạo ra một installer chứa toolchain + sysroot khớp với image. Sau khi cài, source environment-setup-* xuất sẵn CC, CFLAGS, LDFLAGS, PKG_CONFIG_*..., trong đó CC đã gồm --sysroot và các option -march/-mfpu/-mfloat-abi của machine. Đây là cách đảm bảo build ngoài bitbake vẫn khớp rootfs. Xem Yocto.

Checklist khi cross compile bị lỗi

Triệu chứng Nguyên nhân thường gặp Kiểm tra
Exec format error chạy binary ARM trên host x86 (hoặc ngược lại) file
No such file or directory dù file có loader (INTERP) không tồn tại trên target readelf -l, ls /lib/ld-*
version 'GLIBC_x' not found build với libc mới hơn libc của target objdump -T, so sánh sysroot với rootfs
Illegal instruction -march/-mcpu/-mfpu cao hơn CPU thật readelf -A
file in wrong format / incompatible link nhầm library của host -Wl,--verbose, kiểm tra -L
Lỗi cannot find -lfoo library chưa có trong sysroot -print-search-dirs, -print-file-name=libfoo.so
Float sai giá trị/crash lạ float ABI không khớp giữa các module readelf -A ở từng file

Tóm tắt

  • Cross compile = cùng quy trình 4 bước nhưng với toolchain, header và library của target; tên toolchain mang triplet (arm-linux-gnueabihf-).
  • Sysroot gom header + library của target; nó phải khớp với rootfs thật.
  • -march/-mcpu/-mfpu/-mfloat-abi chọn CPU và ABI; float ABI là ràng buộc cho mọi thứ link cùng nhau.
  • file, readelf -h/-A/-l và qemu-user là ba công cụ để kiểm tra nhanh binary có đúng target không.
  • Tích hợp qua CROSS_COMPILE/CC (Makefile), toolchain file (CMake) hay SDK (Yocto).

Tiếp theo: Debug và phân tích.

References