记一次页面对齐导致的异常

背景

在平凡无奇的一天,某个daily build 又挂了,看起来也很正常, 这次是tidb-monitring docker构建失败 docker busybox fail

排查过程

  • 怀疑是build程序有问题,先拿一台物理机重现,手工下载了源文件,运行docker build 一切正常,重现失败
  • 直接重跑流水线,该问题又再次出现,这次去对应的物理机上重试,发现问题能重现,是一台arm构建机
  • 手工重试每个步骤: 直接docker run busybox, 程序退出 报137错误。在其他arm机器上重试,又能成功
  • 137 也就是segment fault错误,怀疑是可执行程序异常,想直接运行二进制试试,先把文件从镜像里面拉出来,由于容器未启动,无法使用docker cp命令,因此采取docker image save的方式导出tar包。取出里面的busybox程序运行,报137错误,将该二进制传输到其他机器,正常。
  • busybox是静态链接的,排除发行版环境导致的问题
[root@k8s-arm-node-4-103 del]# file busybox
busybox: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, stripped

分析两台机器的不同点 先查看cpu

# dmidecode -t processor
...
Processor Information
        Type: Central Processor
        Family: ARM
        Version: Kunpeng 920-4826

两台机器分别是 Kunpeng 920-4826 vs HUAWEI Kunpeng 920 5220,两者看起来差别并不大,可能是也可能不是这个原因

  • Segment fault 一般是异常内存访问导致的,打开core dump分析。打开core dump->运行程序->程序崩溃,但是并没有产生core dump
  • 手工写一个一定会segment fault的程序运行,产生了core dump
  • 使用gdb调试
[root@k8s-arm-node-4-103 del]# gdb busybox
...
Reading symbols from /root/del/busybox...(no debugging symbols found)...done.
(gdb) r
Starting program: /root/del/busybox
During startup program terminated with signal SIGSEGV, Segmentation fault.
(gdb) bt
No stack.
(gdb) info register pc
The program has no registers now.
(gdb)

发现根本看不到程序执行的痕迹,调试我编译的可执行程序在segment fault的时候能够看到stack trace以及正常的PC

  • 在程序入口打断点试试,依旧不行,连程序最初的入口都没执行到,程序根本没到执行阶段,在执行前就遇到问题了
[root@k8s-arm-node-4-103 del]# readelf -h busybox|grep Entry
  Entry point address:               0x400740
[root@k8s-arm-node-4-103 del]# gdb busybox
...
Reading symbols from /root/del/busybox...(no debugging symbols found)...done.
(gdb) b *(0x400740)
Breakpoint 1 at 0x400740
(gdb) r
Starting program: /root/del/busybox
During startup program terminated with signal SIGSEGV, Segmentation fault.
(gdb) info register pc
The program has no registers now.
(gdb)
  • 怀疑是安全系统拦截了,检查selinux,是未启用的,也可能是其他的安全机制例如apparmor
  • 到这儿卡组了一下下。程序根本就没执行就出错,开始怀疑是加载过程,但是这是静态链接的,加载过程比较简单,不太会有链接问题,而且有加载过程的话加载器会报错
  • 突然想到,如果直接用用户态的动态连接器去加载它,会发生什么,报错会不会更友好?通过随便一个动态链接的程序找到系统的动态加载器 /lib/ld-linux-aarch64.so.1
[root@k8s-arm-node-4-103 del]# readelf -l a.out

Elf file type is EXEC (Executable file)
Entry point 0x400510
There are 9 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001f8 0x00000000000001f8  R E    8
  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238
                 0x000000000000001b 0x000000000000001b  R      1
      [Requesting program interpreter: /lib/ld-linux-aarch64.so.1]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000009a4 0x00000000000009a4  R E    10000

调用,这个报错也很有迷惑性,还以为只能加载动态链接的

[root@k8s-arm-node-4-103 del]# /lib/ld-linux-aarch64.so.1 busybox
busybox: error while loading shared libraries: busybox: cannot open shared object file

当然,需要使用绝对地址

[root@k8s-arm-node-4-103 del]# /lib/ld-linux-aarch64.so.1 /root/del/busybox
/root/del/busybox: error while loading shared libraries: /root/del/busybox: ELF load command alignment not page-aligned

真相大白:ELF load command alignment not page-aligned

还是用户态的加载器报错友好呀,内核加载器就扔个segment fault 差点我引入歧途 看看页对齐情况

[root@k8s-arm-node-4-103 del]# readelf -l busybox

Elf file type is EXEC (Executable file)
Entry point 0x400740
There are 5 program headers, starting at offset 64

Program Headers:
Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x000000000012b2f8 0x000000000012b2f8  R E    1000
  LOAD           0x000000000012bdf0 0x000000000052cdf0 0x000000000052cdf0
                 0x0000000000000611 0x0000000000005070  RW     1000
  TLS            0x000000000012bdf0 0x000000000052cdf0 0x000000000052cdf0
                 0x0000000000000000 0x0000000000000008  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     10
  GNU_RELRO      0x000000000012bdf0 0x000000000052cdf0 0x000000000052cdf0
                 0x0000000000000210 0x0000000000000210  R      1
[root@k8s-arm-node-4-103 del]# getconf PAGESIZE
65536
[root@172 ~]# getconf PAGESIZE
4096

busybox可执行程序是按照 4k对齐的,但是出问题的机器页面是64k,其他机器是4k

触发原因

我们的代码没有任何改动,而是依赖的dockerhub基础镜像有变动 dockerhub busybox changlog Dockerhub官方busybox进行了覆盖更新,覆盖后的会出问题, 已经向dockerhub/busybox项目提了issue

解决方案

  • builder可以调小页面,大页面对build性能没多大帮助
    • 发现有不少机器都是64k页面对齐的,更新builder能构建出镜像,但是镜像运行时的兼容性存疑
  • 更换基础镜像
    • 外部基础镜像也存在不规范更新的情况,建议以后全部采用内部基础镜像