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...
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:
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:
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:
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:
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-abichọn CPU và ABI; float ABI là ràng buộc cho mọi thứ link cùng nhau.file,readelf -h/-A/-lvàqemu-userlà 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¶
man arm-linux-gnueabihf-gcc(cùng option vớiman gcc), GCC ARM Options- GCC AArch64 Options
- CMake: cmake-toolchains(7)
- Yocto Project: Using the Extensible SDK / Standard SDK