第一章:Go交叉编译的核心原理与环境约束
Go 的交叉编译能力源于其静态链接特性和平台无关的构建系统设计。编译器在构建阶段直接嵌入目标操作系统和架构所需的运行时、标准库及 C 兼容层(如 libc 或 musl),无需依赖目标主机的本地工具链,从而实现“一次编写、多端构建”。
构建过程的关键机制
Go 编译器通过 GOOS 和 GOARCH 环境变量控制目标平台,例如 linux/amd64、darwin/arm64 或 windows/386。它不调用外部 C 编译器(如 gcc)生成目标代码,而是使用内置的 SSA 后端直接生成对应架构的机器码;仅当启用 cgo 时,才需匹配目标平台的 C 工具链。
环境约束与兼容性限制
cgo启用时,必须安装对应目标平台的交叉编译工具链(如aarch64-linux-gnu-gcc),且CC_${GOOS}_${GOARCH}环境变量需正确指向该编译器- Windows 目标不支持
CGO_ENABLED=1下链接 POSIX C 库;Linux 目标若需最小体积,推荐禁用 cgo:CGO_ENABLED=0 go build - 某些标准库功能存在平台差异(如
os/user.LookupId在 Windows 上不可用),需通过构建标签(//go:build !windows)隔离
实际构建示例
以下命令在 macOS 主机上构建 Linux ARM64 可执行文件(无 cgo 依赖):
# 设置目标平台并构建
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o app-linux-arm64 .
# 验证输出格式(需安装 file 命令)
file app-linux-arm64
# 输出示例:app-linux-arm64: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, Go BuildID ...
支持的目标平台矩阵(部分)
| GOOS | GOARCH | 是否默认支持 | 备注 |
|---|---|---|---|
| linux | amd64 | ✅ | 默认构建平台 |
| darwin | arm64 | ✅ | Apple Silicon 原生支持 |
| windows | 386 | ✅ | 32 位 Windows |
| freebsd | riscv64 | ❌ | 需 Go 1.21+ 且手动启用 |
交叉编译结果为纯静态二进制,但若引入 net 包且 CGO_ENABLED=1,则可能动态链接 libc,破坏可移植性——因此生产环境强烈建议显式设置 CGO_ENABLED=0 并验证符号依赖。
第二章:ARM64平台交叉编译的深度避坑指南
2.1 GOARCH=arm64与CPU特性兼容性理论解析及QEMU实测验证
ARM64 架构下,GOARCH=arm64 编译的二进制依赖 CPU 支持特定扩展(如 CRC32, AES, LSE 原子指令)。Go 运行时通过 getauxval(AT_HWCAP) 动态检测能力,而非硬编码假设。
QEMU 启动关键参数
qemu-system-aarch64 \
-cpu cortex-a72,features=+crc,+aes,+lse \
-machine virt,gic-version=3 \
-kernel ./Image \
-initrd ./initramfs.cgz
-cpu cortex-a72,features=...显式启用硬件能力,避免 Go 运行时误判为缺失;gic-version=3确保中断控制器兼容 ARMv8.1+ 的 LSE 原子操作前提。
兼容性检测逻辑
// runtime/internal/sys/arch_arm64.go(简化)
const HasLSE = buildcfg.GOARM64_HAS_LSE // 由构建时或运行时动态填充
Go 在启动阶段调用 cpu.Initialize(),读取 /proc/cpuinfo 或 AT_HWCAP,设置 atomic.Is64Bit 和 atomic.HasLSE 标志。
| 特性 | Linux HWCAP bit | Go 运行时影响 |
|---|---|---|
CRC32 |
HWCAP_CRC32 |
hash/crc32 使用硬件加速 |
LSE |
HWCAP_ATOMICS |
sync/atomic 跳过锁回退路径 |
graph TD
A[Go 程序启动] --> B{读取 AT_HWCAP}
B -->|含 HWCAP_ATOMICS| C[启用 LSE atomic 指令]
B -->|不含| D[降级为 LL/SC 循环]
C --> E[高性能 CAS/ADD]
2.2 CGO_ENABLED=0模式下C标准库缺失引发的运行时panic复现与修复
当 CGO_ENABLED=0 编译时,Go 运行时无法调用 libc(如 getaddrinfo, clock_gettime),导致某些标准库函数在运行时 panic。
复现场景
// main.go
package main
import (
"net"
"fmt"
)
func main() {
_, err := net.LookupIP("example.com") // 触发 cgo 依赖的 DNS 解析
if err != nil {
panic(err) // 在 CGO_ENABLED=0 下 panic: "lookup example.com: no such host"
}
fmt.Println("OK")
}
此代码在
CGO_ENABLED=0 go build后执行会因无 libc 支持的 DNS 解析器而 panic。Go 默认启用纯 Go DNS 解析器(GODEBUG=netdns=go),但若环境变量被覆盖或平台受限,仍可能 fallback 失败。
修复策略对比
| 方案 | 是否需重编译 | 是否兼容 Alpine | 风险 |
|---|---|---|---|
GODEBUG=netdns=go |
否 | ✅ | 仅影响 DNS,无 libc 依赖 |
CGO_ENABLED=1 |
是 | ❌(Alpine 需 musl-dev) |
引入动态链接依赖 |
| 自定义 Resolver | 是 | ✅ | 需实现 net.Resolver 接口 |
根本解决流程
graph TD
A[CGO_ENABLED=0] --> B{DNS 查询触发}
B --> C[尝试 libc getaddrinfo]
C --> D[失败 → panic]
D --> E[强制启用纯 Go resolver]
E --> F[成功解析]
2.3 ARM64内存对齐要求对struct布局的影响分析与binary.Size校验实践
ARM64架构强制要求基本类型按自身大小对齐(如int64需8字节对齐),结构体整体对齐值为其最大字段对齐值,字段间可能插入填充字节。
struct内存布局示例
type Example struct {
A uint8 // offset 0
B uint64 // offset 8 (跳过7字节填充)
C uint32 // offset 16
} // size=24, align=8
B必须从8字节边界开始,导致A后填充7字节;C自然对齐于16,无额外填充;总大小24字节。
binary.Size校验实践
binary.Size(&Example{})返回24,反映真实序列化长度- 若忽略对齐,直接按字段累加(1+8+4=13)将导致读写越界
| 字段 | 类型 | 偏移 | 填充 |
|---|---|---|---|
| A | uint8 | 0 | — |
| — | pad | 1–7 | 7B |
| B | uint64 | 8 | — |
| C | uint32 | 16 | — |
对齐敏感场景
- 跨平台RPC序列化必须统一填充策略
- mmap共享内存需确保所有CPU架构视图一致
unsafe.Sizeof与binary.Size结果一致,验证布局合规
2.4 Linux内核版本与syscall ABI差异导致的系统调用失败定位与补丁注入
当用户态程序在较新内核(如6.1)上运行旧二进制时,clock_gettime(CLOCK_MONOTONIC_RAW, ...) 可能返回 -ENOSYS——因该 syscall 在 v5.11+ 才稳定导出 ABI,旧 glibc 未回退至 clock_gettime64。
定位差异:检查 syscall 号与 ABI 稳定性
// 查看当前内核中 clock_gettime 的实际号(x86_64)
#include <asm/unistd_64.h>
printf("SYS_clock_gettime = %d\n", __NR_clock_gettime); // v5.10: 228, v5.11+: 403(重映射)
该代码输出揭示 ABI 断层点:v5.11 起 __NR_clock_gettime 被重定义为 __NR_clock_gettime64(403),而旧二进制仍硬编码 228,触发 sys_ni_syscall。
补丁注入路径
- 动态:
ptrace拦截execve后,mmap注入 shim 页,重写.text中 syscall 指令为目标号; - 静态:
patchelf --replace-needed+ 自定义 libc stub。
| 内核版本 | clock_gettime syscall 号 |
ABI 状态 |
|---|---|---|
| ≤5.10 | 228 | legacy-only |
| ≥5.11 | 403 (clock_gettime64) |
stable ABI |
graph TD
A[进程执行 syscall 228] --> B{内核查表 sys_call_table[228]}
B -->|v5.10| C[指向有效 handler]
B -->|v5.11+| D[指向 sys_ni_syscall]
D --> E[返回 -ENOSYS]
2.5 跨平台调试符号strip后无法gdb远程调试的完整链路还原与dwz优化方案
问题链路还原
当交叉编译目标(如 aarch64-linux-gnu-gcc)生成二进制后执行 strip --strip-debug,.debug_* 段被移除,但 .symtab 和 .strtab 仍残留——gdbserver 依赖 .symtab 加载符号地址映射,而远程 gdb 因缺失 .debug_info 无法解析变量/源码行号。
# 典型 strip 命令(错误示范)
aarch64-linux-gnu-strip --strip-debug --strip-unneeded app
# --strip-debug:删 .debug_* 段;--strip-unneeded:删 .symtab/.strtab → 导致 gdbserver 启动失败
此命令误删
.symtab,使 gdbserver 无法构建符号表索引;正确做法应保留.symtab仅剥离调试段。
dwz 优化路径
使用 dwz 合并重复调试信息,再 strip --strip-debug 保留符号表:
| 工具 | 作用 | 是否保留 .symtab |
|---|---|---|
strip -g |
删除全部调试信息(含 .symtab) | ❌ |
strip --strip-debug |
仅删 .debug_* 段 | ✅ |
dwz -m merged.dwo app |
压缩调试信息至独立文件 | ✅(原文件不变) |
graph TD
A[原始ELF] --> B[dwz -m app.dwo app]
B --> C[strip --strip-debug app]
C --> D[app + app.dwo]
D --> E[gdb --se=app --sym=app.dwo]
第三章:Windows目标文件生成的关键陷阱
3.1 GOOS=windows下PE头签名与TLS回调函数冲突的静态链接原理与/MDvs/MT实测对比
当使用 GOOS=windows 构建 Go 程序并启用 TLS 回调(如 via //go:build windows + __declspec(thread) 或 Cgo 引入的 TLS 初始化)时,PE 文件的 .tls 节与数字签名存在字节对齐冲突:签名覆盖 TLS 目录项(IMAGE_DIRECTORY_ENTRY_TLS),导致 Windows 加载器跳过 TLS 回调执行。
链接器行为差异
/MT:静态链接 CRT,TLS 数据直接嵌入.tls节,签名易破坏其 RVA/Size 字段;/MD:动态链接msvcrtd.dll,TLS 初始化由 DLL 自行注册,绕过 PE TLS 目录依赖。
实测关键参数对比
| 配置 | TLS 回调触发 | PE 签名完整性 | 启动延迟(ms) |
|---|---|---|---|
/MT |
❌(签名后失效) | ✅(但 TLS 失效) | 12.4 |
/MD |
✅(DLL 侧注册) | ✅(签名不干扰 TLS) | 18.7 |
// 示例:TLS 回调声明(需在 .def 文件中导出)
#pragma comment(linker, "/INCLUDE:_tls_used")
#pragma comment(linker, "/SECTION:.tls,ERW")
extern "C" void NTAPI tls_callback(PVOID, DWORD reason, PVOID) {
if (reason == DLL_PROCESS_ATTACH) { /* 初始化逻辑 */ }
}
该代码强制生成 .tls 节并注册回调;/MT 下链接器将其写入 PE TLS 目录,而签名工具(如 signtool.exe)重写 OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS],清零 VirtualAddress 字段,使回调永不触发。
graph TD
A[Go build -ldflags '-H windowsgui'] --> B[CGO_ENABLED=1]
B --> C{Linker Mode}
C -->|/MT| D[.tls 节直写 PE TLS Directory]
C -->|/MD| E[Defer to msvcrtd!_init_thread_tls]
D --> F[SignTool overwrite TLS Dir → callback ignored]
E --> G[Signature leaves TLS Dir intact → callback runs]
3.2 Windows路径分隔符与embed.FS在交叉编译时的字符串字面量编码失效问题复现与go:embed元数据修正
当在 Linux/macOS 上交叉编译 Windows 目标(GOOS=windows)时,//go:embed 指令中含反斜杠的路径(如 assets\config.json)会被 Go 工具链错误解析为转义序列,导致 embed.FS 无法定位文件。
复现代码
//go:embed assets\config.json // ❌ 反斜杠被当作转义符处理
var configFS embed.FS
逻辑分析:Go 1.16+ 的
embed包在构建阶段静态解析字符串字面量;\c被解释为控制字符(如\t,\n),而非路径分隔符,致使assets\config.json实际被解析为非法 Unicode 序列,嵌入失败。
正确写法(统一使用正斜杠)
//go:embed assets/config.json // ✅ 所有平台兼容,embed.FS 内部自动适配
var configFS embed.FS
| 场景 | 路径写法 | 是否生效 | 原因 |
|---|---|---|---|
| Linux 主机 + windows 目标 | assets/config.json |
✅ | embed.FS 规范要求 POSIX 路径 |
| Windows 主机本地构建 | assets\config.json |
⚠️(仅限原生构建) | go toolchain 在 Windows 上容忍但不推荐 |
交叉编译含 \ 字面量 |
assets\config.json |
❌ | 字符串字面量被提前解码,元数据损坏 |
graph TD A[源码中 //go:embed assets\config.json] –> B[go build 解析字符串字面量] B –> C{是否含未转义反斜杠?} C –>|是| D[触发转义解码 → \c → 非法码点] C –>|否| E[保留原始路径 → embed.FS 正确映射]
3.3 MinGW-w64工具链与Go原生linker协同生成GUI程序时资源节(.rsrc)丢失的二进制结构分析与ldflags注入实践
当使用 go build -ldflags="-H windowsgui" 编译 GUI 程序时,Go 原生 linker 会剥离 .rsrc 节——因其默认不识别 MinGW-w64 链接器注入的 Windows 资源段。
资源节丢失的根本原因
Go linker 仅保留其自身理解的 COFF/PE 节区(如 .text, .data),而 MinGW-w64 生成的 .rsrc 节在 go link 阶段被静默丢弃。
修复方案:ldflags 注入资源节保留指令
go build -ldflags="-H windowsgui -extldflags '-Wl,--enable-long-section-names'" main.go
-extldflags将参数透传给 MinGW-w64 的ld;--enable-long-section-names确保.rsrc节名不被截断(避免 Go linker 因节名非法而跳过)。
关键参数对照表
| 参数 | 作用 | 是否必需 |
|---|---|---|
-H windowsgui |
设置子系统为 Windows GUI,禁用控制台窗口 | ✅ |
-extldflags '-Wl,--enable-long-section-names' |
启用长节名支持,保留 .rsrc |
✅ |
-extldflags '-Wl,--add-stdcall-alias' |
兼容 stdcall 符号导出 | ⚠️(按需) |
PE 结构验证流程
graph TD
A[go build] --> B[MinGW-w64 ld 链接 .rsrc]
B --> C[Go linker 扫描节区]
C --> D{节名长度 ≤ 8?}
D -->|否| E[丢弃 .rsrc]
D -->|是| F[保留并写入最终 PE]
第四章:mips64及其他小众架构的编译突围策略
4.1 GOARCH=mips64le下浮点ABI(o32/n32/n64)选择错误导致math包精度异常的汇编级溯源与buildmode=shared适配
MIPS64LE平台默认启用n32 ABI,但Go runtime在buildmode=shared时未显式约束浮点调用约定,导致math.Sqrt等函数误用o32软浮点寄存器映射($f0–$f15),而实际硬件期望n32/n64的$f0–$f31双字对齐布局。
ABI寄存器映射差异
| ABI | 浮点寄存器总数 | 传参寄存器 | 双精度对齐要求 |
|---|---|---|---|
| o32 | 16 | $f12–$f15 |
无 |
| n32/n64 | 32 | $f12–$f19 |
强制8-byte |
汇编级异常示例
// 错误:o32 ABI下Sqrt调用(截断高32位)
mov.d $f12, $f13 // 仅复制低32位 → 精度丢失
jal sqrt // 调用o32版libc sqrt
该指令将64位double拆分为两个32位字操作,破坏IEEE 754双精度表示。
修复方案
- 编译时强制指定:
GOARM=0 CGO_ENABLED=1 GOARCH=mips64le GOABIFLAGS=-mabi=n64 - 修改
src/runtime/asm_mips64x.s中float64参数传递逻辑,适配n64ABI寄存器窗口
# 验证ABI一致性
go build -buildmode=shared -ldflags="-linkmode external" -gcflags="-l" .
此命令触发链接器校验浮点符号重定位表,暴露ABI不匹配错误。
4.2 MIPS64指令集扩展(MSA)未启用时SIMD加速失效的编译器标志组合验证与//go:build mips64约束声明实践
当目标平台为 mips64 但未启用 MSA(MIPS SIMD Architecture)扩展时,Go 编译器无法生成 SIMD 指令,导致 crypto/aes、encoding/binary 等依赖向量优化的包退化为标量实现。
编译器标志冲突验证
以下标志组合将隐式禁用 MSA 支持:
# ❌ 错误:-march=mips64r2 不含 MSA;-mno-msa 显式关闭
GOOS=linux GOARCH=mips64 CGO_ENABLED=1 \
CC=mips64-linux-gnu-gcc \
CFLAGS="-march=mips64r2 -mno-msa -msoft-float" \
go build -buildmode=exe -o app .
分析:
-march=mips64r2仅定义基础 ISA 版本(无 MSA),而-mno-msa强制屏蔽扩展;-msoft-float进一步禁用硬件浮点单元——三者叠加使所有//go:build mips64 && msa条件失效。
构建约束声明实践
| 约束表达式 | 匹配场景 | 是否启用 MSA |
|---|---|---|
//go:build mips64 |
所有 MIPS64 平台(含无 MSA) | ❌ |
//go:build mips64 && msa |
仅 MSA 启用的 MIPS64(需工具链支持) | ✅ |
构建路径决策流程
graph TD
A[GOARCH=mips64] --> B{MSA enabled in toolchain?}
B -->|Yes| C[Use //go:build mips64 && msa]
B -->|No| D[Fallback to scalar paths<br>//go:build mips64 && !msa]
4.3 交叉编译mips64目标在LoongArch过渡期的ABI兼容层构建:从syscall重定向到cgo shim封装
在LoongArch生态演进中,为保障存量mips64二进制兼容性,需构建轻量级ABI适配层。核心策略分两阶段:底层系统调用重定向与上层C接口封装。
syscall重定向机制
通过__kernel_syscall跳转表劫持mips64 ABI约定的syscall号,映射至LoongArch内核实际入口:
// mips64_syscall_redirect.S
.globl __mips64_syscall_redirect
__mips64_syscall_redirect:
li a7, 0x123 // mips64 sys_write → LoongArch sys_write (0x123)
jal __loongarch_syscall
ret
a7寄存器承载重映射后的LoongArch syscall号;__loongarch_syscall为统一内核入口,避免重复陷入。
cgo shim封装
Go代码通过//export暴露C符号,由shim桥接mips64 ABI参数布局:
| mips64 ABI | LoongArch ABI | 转换方式 |
|---|---|---|
$a0-$a3 |
a0-a3 |
寄存器直传 |
$a4,$a5 |
a4,a5 |
栈偏移补位 |
//export mips64_open
func mips64_open(path *C.char, flags int, mode uint32) int {
// 参数重排:mips64将mode放$a5,LoongArch放a2
return C.loongarch_open(C.CString(C.GoString(path)), C.int(flags), C.uint(mode))
}
该shim确保cgo调用链不破坏栈帧对齐,且规避-fno-stack-protector等编译器差异。
graph TD
A[mips64用户态] --> B[syscall指令]
B --> C{ABI兼容层}
C --> D[syscall重定向表]
C --> E[cgo shim函数]
D --> F[LoongArch内核]
E --> F
4.4 小端/大端混合环境中binary.Read/write字节序错乱的接口契约验证与encoding/binary.ByteOrder显式声明规范
字节序隐式依赖的风险根源
binary.Read/Write 默认使用 binary.LittleEndian,但未在函数签名中体现,导致跨平台序列化时契约不透明。
显式声明的强制实践
必须显式传入 ByteOrder 实例,禁止依赖包级默认值:
// ✅ 正确:显式声明,契约清晰
err := binary.Read(r, binary.BigEndian, &header)
// ❌ 危险:隐式依赖 LittleEndian,易在ARM64(小端)与PowerPC(大端)混合部署中失败
err := binary.Read(r, nil, &header) // 编译错误,但实际中常被忽略
逻辑分析:
binary.Read第三个参数为interface{},但底层通过反射获取字段大小并按指定ByteOrder解析;若省略或误用,结构体字段将被逆序解释,例如uint32(0x12345678)在大端机器上被读作0x78563412。
接口契约验证清单
- [ ] 所有
binary.Read/Write调用均含非-nilByteOrder参数 - [ ] 协议文档明确标注字节序(如 “Header.Length: uint16, big-endian”)
- [ ] 单元测试覆盖小端/大端模拟环境(
binary.BigEndianvsbinary.LittleEndian)
| 场景 | 推荐 ByteOrder | 说明 |
|---|---|---|
| 网络协议(TCP/IP) | binary.BigEndian |
符合网络字节序(RFC 1700) |
| Windows PE 文件 | binary.LittleEndian |
x86/x64 架构原生格式 |
| 跨架构 RPC 消息 | binary.BigEndian |
避免端序歧义 |
graph TD
A[调用 binary.Read] --> B{ByteOrder 显式传入?}
B -->|否| C[字节序错乱风险]
B -->|是| D[按指定顺序解析字段]
D --> E[契约可验证、可测试]
第五章:Go 1.22+新特性对交叉编译生态的颠覆性影响
构建约束系统的原生化重构
Go 1.22 引入 //go:build 的隐式继承机制与 GOOS=auto 推断能力,彻底替代了过去依赖 GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build 的脆弱组合。某边缘AI推理服务团队将构建脚本从 17 行 shell 循环压缩为单行:go build -o bin/infer-linux-arm64 ./cmd/infer --os=linux --arch=arm64,构建失败率从 12% 降至 0.3%,根本原因在于编译器现在能主动校验 cgo 依赖在目标平台的 ABI 兼容性,而非静默跳过或崩溃。
环境变量驱动的跨平台模块裁剪
通过 GOEXPERIMENT=loopvar,fieldtrack 启用的新构建标签系统,开发者可在 build/constraints_linux.go 中声明:
//go:build linux && !cgo
package main
import _ "net/http/pprof" // 仅 Linux 静态链接时启用 pprof
某 IoT 网关项目据此将 ARMv7 镜像体积从 48MB 削减至 22MB——关键在于 Go 1.22 的 linker 现在能识别并剥离未被任何 //go:build 路径引用的符号表,而旧版需手动维护 // +build ignore 注释块。
多平台二进制的统一发布流水线
| 构建方式 | Go 1.21 | Go 1.22+ | 差异说明 |
|---|---|---|---|
GOOS=windows go build |
生成 .exe | 生成 .exe + 自动嵌入 manifest | 支持 Windows UAC 提权声明 |
GOOS=darwin go build |
需手动 codesign | 自动生成 hardened runtime | 默认启用 --entitlements 清单 |
GOOS=freebsd go build |
依赖外部 libc | 静态链接 musl 兼容层 | FreeBSD 14+ 内核调用零适配 |
某开源 CLI 工具采用此机制后,GitHub Actions 的发布作业从 9 个并发 job 缩减为 3 个,且 macOS 版本首次通过 Apple Gatekeeper 审核。
CGO 交叉编译的可信链验证
Go 1.22.3 新增 go env -w CGO_CROSS_COMPILE=1 模式,强制要求所有 C 依赖必须通过 CC_for_target 指定交叉编译器,并自动校验 .a 库的 ELF e_machine 字段。某区块链节点项目在迁移到该模式后,捕获到此前被忽略的 x86_64 OpenSSL 库误用于 aarch64 构建的问题——错误发生在 CI 阶段而非生产环境 panic。
flowchart LR
A[源码含#cgo] --> B{GOOS/GOARCH设置}
B -->|Linux/amd64| C[调用x86_64-linux-gnu-gcc]
B -->|Windows/arm64| D[调用clang-cl.exe --target=arm64-windows-msvc]
C --> E[生成静态libcrypto.a]
D --> F[生成动态crypt32.dll导入库]
E & F --> G[linker注入平台专属符号重定向表]
构建缓存的跨架构一致性保障
Go 1.22 的 build cache 现在以 (GOOS, GOARCH, CC, CGO_ENABLED) 四元组为 key 进行分片,避免了旧版中因 CC=gcc 在不同平台复用导致的 ABI 混淆。某金融交易网关的 CI 流水线实测显示,ARM64 构建缓存命中率从 41% 提升至 89%,因为 linker 不再需要重复解析 __float128 类型在不同 ABI 下的内存布局差异。
