0. 序
在我的观感上来说吧,这11章写得真是灾难。灾难体现在两个地方,一个讲的实现机制比较古老了,比如.ctor
段这些gcc早已经不再使用了。另一个是glibc
和windows
的MSVC
混着讲,看着挺不舒服的(也许是因为我把前面讲windows的地方都跳过了)。
为了更实际地看看C库与C编译器是如何配合,完成C代码的初始化与运行的,尝试编译一个带调试信息的C库,手动调试是一个不错的选择。
1. 从源码编译C库
比较了几种C库的实现,最终选择了riscv-musl
,选riscv
主要是因为这个体系结构比较新,没有多少历史包袱,实现上可能会比较干净。下面的安装参考官网的docs
$ git clone https://github.com/riscvarchive/riscv-musl
$ cd riscv-musl
$ ./configure --prefix=/home/ckf/test/riscv-musl/build/ --syslibdir=/home/ckf/test/riscv-musl/build/lib --build=x86_64 --host=riscv64 --enable-debug CROSS_COMPILE=riscv64-linux-gnu-
# --prefix指定安装C库的基础目录,放在一个临时的目录里就好了
# --syslibdir指定动态链接器的位置
# --build 指定当前运行的平台
# --host 指定编译后的运行平台
# --target 指定该库的运行目标,比如说我们希望在X86环境下编译gcc,然后
# 让gcc运行在arm平台下,并且编译出risc-v的代码,那么就需要指定为
# --build=x86_64 --host=arm --target=riscv 这样,默认情况下target
# 等于host.
# 另外我们编译时保留调试信息,并且指定交叉编译的工具链。注意这里需要使用
# riscv-linux-gnu-, 之前试了一下riscv-unknown-elf-, 发现对应的ld
# 不支持动态链接.
$ make
$ make install
经过以上步骤,我们就得到了一个带调试信息的libc.so
,可以在build/lib
目录下检查C库是否有调试信息,出现了相应的.debug_*
节就表示有调试信息。
$ readelf -S libc.so
···
[17] .debug_aranges PROGBITS 0000000000000000 0008a520
0000000000011ac0 0000000000000000 0 0 16
[18] .debug_info PROGBITS 0000000000000000 0009bfe0
0000000000103eb0 0000000000000000 0 0 1
[19] .debug_abbrev PROGBITS 0000000000000000 0019fe90
000000000005ba2b 0000000000000000 0 0 1
[20] .debug_line PROGBITS 0000000000000000 001fb8bb
0000000000073782 0000000000000000 0 0 1
[21] .debug_frame PROGBITS 0000000000000000 0026f040
0000000000018c80 0000000000000000 0 0 8
[22] .debug_str PROGBITS 0000000000000000 00287cc0
0000000000010ba9 0000000000000001 MS 0 0 1
[23] .debug_loc PROGBITS 0000000000000000 00298869
00000000000bdf0d 0000000000000000 0 0 1
[24] .debug_ranges PROGBITS 0000000000000000 00356776
···
另外可以看到,lib
目录下生成的动态链接器只是一个指向libc.so
的符号链接,这说明在musl-C
里libc.so
本身就是动态链接器!
由于gcc的配置与C库是紧耦合的,如果我们直接编译时指定rpath
让gcc链接到我们刚编译好的C库有可能会出错(因为即使C库换了,gcc的crt*.o
文件还是用的原来的配置)。因此musl-C
在安装目录下提供了一个对编译器的包装bin/musl-gcc
$ cat musl-gcc
#!/bin/sh
exec "${REALGCC:-riscv64-linux-gnu-gcc}" "$@" -specs "/home/ckf/test/riscv-musl/build/lib/musl-gcc.specs"
$ $ cat ../lib/musl-gcc.specs
%rename cpp_options old_cpp_options
*cpp_options:
-nostdinc -isystem /home/ckf/test/riscv-musl/build/include -isystem include%s %(old_cpp_options)
*cc1:
%(cc1_cpu) -nostdinc -isystem /home/ckf/test/riscv-musl/build/include -isystem include%s
*link_libgcc:
-L/home/ckf/test/riscv-musl/build/lib -L .%s
*libgcc:
libgcc.a%s %:if-exists(libgcc_eh.a%s)
*startfile:
%{!shared: /home/ckf/test/riscv-musl/build/lib/Scrt1.o} /home/ckf/test/riscv-musl/build/lib/crti.o crtbeginS.o%s
*endfile:
crtendS.o%s /home/ckf/test/riscv-musl/build/lib/crtn.o
*link:
-dynamic-linker /home/ckf/test/riscv-musl/build/lib/ld-musl-riscv64.so.1 -nostdlib %{shared:-shared} %{static:-static} %{rdynamic:-export-dynamic}
*esp_link:
*esp_options:
*esp_cpp_options:
这里先不去细究上面是怎么回事的。
2. C库调试初体验
// havea.c
// musl-gcc -g -o havea havea.c
#include <stdio.h>
#include <unistd.h>
int a;
void init() __attribute__ ((constructor(540)));
void init(){
a = 7;
write(1, "abcde", 5);
printf("%s\n", __FUNCTION__);
}
int main(int argc, char* argv[]){
printf("%d\n", a);
//print_hello(argv[0]);
//sleep(7200);
return 0;
}
用musl-gcc
把上面的havea.c
编译为可执行文件后,我们用qemu-riscv64
和gdb-multiarch
进行调试。
$ cat .gdbinit
set architecture riscv:rv64
target remote localhost:10000
layout split
# 主要省得每次都输入这些命令了
$ qemu-riscv64 -g 10000 havea
# 然后另外开启一个终端
$ gdb-multiarch
(gdb) file havea
这下应该就可以看到汇编代码了。预期首先看到的是动态链接器_dlstart
函数,因为动态链接器首先拿到程序的控制权,_dlstart
函数是用汇编写的,所以这一段没有源码。对应的源码目录是在ldso/dlstart.c
。
#include <stddef.h>
#include "dynlink.h"
#include "libc.h"
#ifndef START
#define START "_dlstart"
#endif
#define SHARED
#include "crt_arch.h"
#ifndef GETFUNCSYM
#define GETFUNCSYM(fp, sym, got) do { \
hidden void sym(); \
static void (*static_func_ptr)() = sym; \
__asm__ __volatile__ ( "" : "+m"(static_func_ptr) : : "memory"); \
*(fp) = static_func_ptr; } while(0)
#endif
hidden void _dlstart_c(size_t *sp, size_t *dynv)
{
// ...
上面的头文件crt_arch.h
对应了_dlstart
的源码,该文件位于arch/riscv64
__asm__(
".text\n"
".global " START "\n"
".type " START ",%function\n"
START ":\n"
".weak __global_pointer$\n"
".hidden __global_pointer$\n"
".option push\n"
".option norelax\n\t"
"lla gp, __global_pointer$\n"
".option pop\n\t"
"mv a0, sp\n"
".weak _DYNAMIC\n"
".hidden _DYNAMIC\n\t"
"lla a1, _DYNAMIC\n\t"
"andi sp, sp, -16\n\t"
"jal " START "_c"
);
一段si
后,我们进入到_dlstart_c
函数,这时候应该能看到源码了。
如果打上断点_start
,便可以进入源程序的调试了。
OK,暂时写到这里。