glibc的编译、安装和调试

glibc的编译、安装和调试

前言

前一段时间看了一些关于linux内核中spinlock的文章,很好奇pthread_spin_lock是如何实现的。在google上也搜索了一下,但给出的均是spinlock的实现原理。因此我决定动手安装一个可调式的glibc,通过debug观察一下pthread_spin_lock是如何实现的。

编译和安装glibc

查看系统已安装的glibc版本

为了确保能够顺利安装成功,选择了和系统已有glibc相同的版本。

$ ldd --version # 查看系统已安装的glibc的版本

ldd (Ubuntu GLIBC 2.32-0ubuntu3) 2.32

Copyright (C) 2020 Free Software Foundation, Inc.

This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Written by Roland McGrath and Ulrich Drepper.

可以观察到系统当前glibc的版本为2.32。

下载并解压glibc

wget http://ftp.gnu.org/gnu/glibc/glibc-2.32.tar.gz # 下载

tar -xvf glibc-2.32.tar.gz #解压

编译、安装glibc

cd glibc-2.32 # 进入解压后的glibc目录

mkdir build

mkdir /opt/glibc-2.32 # 创建glibc的安装目录

cd build

../configure --prefix=/opt/glibc-2.32 --enable-debug # 设置库的安装位置和添加调试信息

make CFLAGS="-g -Og" # 以-g形式编译,将调试信息添加可执行文件和共享库中

make install # 安装glibc

export LD_LIBRARY_PATH=/opt/glibc-2.32/lib:$LD_LIBRARY_PATH # 指定glibc库的动态连接库路径

注:

如果configure失败,记得执行make distclean或者make clean清理配置信息;

我在编译时并没有一次编译成功,期间遇到了一些报错。这些报错主要是因为编译器将一些warning视为了error,这时只需添加编译选项-Wno-error=[类型名称],比如-Wno-error=attribute-alias,-Wno-error=maybe-uninitialized。这样一来,只需重新执行make CFLAGS="-g -Og -Wno-error=maybe-uninitialized -Wno-error=maybe-uninitialized"即可。

--enable-debug和-g的区别:--enable-debug 和 -g 是两个不同的选项,用于不同的目的:

--enable-debug:这是 glibc 配置过程中的一个选项。当使用--enable-debug 选项进行配置时,glibc 将以调试模式编译,以确保 glibc 库本身具有调试信息和符号。这些调试信息和符号有助于在调试 glibc 库时,能够定位问题、跟踪函数调用和查看变量的值。--enable-debug 的作用是为了在 glibc 库本身中启用调试支持。

-g:这是编译器(例如 gcc)的选项。当使用 -g 选项进行编译时,编译器会在生成的可执行文件和共享库中添加调试信息。这些调试信息允许调试器在执行程序时,能够准确地定位源代码文件和行号,以便进行源代码级别的调试。-g 的作用是为了在可执行文件和共享库中添加调试信息,方便在调试器中进行调试。

总结:--enable-debug 是用于 glibc 的配置选项,启用调试模式以确保 glibc 库本身具有调试信息和符号。-g 是编译器选项,用于在生成的可执行文件和共享库中添加调试信息,以便在调试器中进行调试。使用这两个选项可以在调试 glibc 库本身以及使用 glibc 库的程序时,提供更好的调试支持。

配置vscode

配置c_cpp_properties.json

{

"configurations": [

{

"name": "Linux",

"includePath": [

"${workspaceFolder}/**",

"/opt/glibc-2.32/include" // 添加 glibc 头文件所在的路径

],

"defines": [],

"compilerPath": "/usr/bin/g++",

"cStandard": "c17",

"cppStandard": "c++17",

"intelliSenseMode": "linux-gcc-x64"

}

],

"version": 4

}

设置了includePath之后,就可以跳转到自己安装的glibc库的头文件。这里还设置了compilerPath、cppStandard等。

配置launch.json

{

"version": "0.2.0",

"configurations": [

{

"name": "C++ Launch",

"type": "cppdbg",

"request": "launch",

"program": "${workspaceFolder}/code/a.out", // 可执行文件的路径

"args": [], // 程序运行时的参数,可以根据需要添加

"cwd": "${workspaceFolder}",

"stopAtEntry": false,

"environment": [

{

"name": "LD_LIBRARY_PATH",

"value": "/opt/glibc-2.32/lib:$LD_LIBRARY_PATH"

}

],

"externalConsole": false,

"MIMode": "gdb",

"miDebuggerPath": "gdb", // GDB 路径,确保 GDB 已正确配置

"setupCommands": [

{

"description": "Enable pretty-printing for gdb",

"text": "-enable-pretty-printing",

"ignoreFailures": true

}

],

"sourceFileMap": {

"/build/glibc/src": "/home/lihao/os_note/denpendcy/glibc-2.32" // 将调试器加载的路径映射到源代码路径

},

"preLaunchTask": "build" // 调试之前执行的构建任务,根据需要指定,

}

]

}

其中environment指定了动态链接库路径LD_LIBRARY_PATH,sourceFileMap指明了glibc源代码的路径。这样一来,调试时就可以调转到源代码的位置。

编写并调试一个关于pthread_spin_lock的demo

demo示例

#include

#include

pthread_spinlock_t lock;

int shared_resource = 0;

void* thread_function(void* arg) {

int i;

for (i = 0; i < 1000000; ++i) {

// 尝试获取自旋锁

pthread_spin_lock(&lock);

shared_resource++;

// 释放自旋锁

pthread_spin_unlock(&lock);

}

return NULL;

}

int main() {

// 初始化自旋锁

pthread_spin_init(&lock, PTHREAD_PROCESS_PRIVATE);

pthread_t thread1, thread2;

// 创建两个线程

pthread_create(&thread1, NULL, thread_function, NULL);

pthread_create(&thread2, NULL, thread_function, NULL);

// 等待两个线程结束

pthread_join(thread1, NULL);

pthread_join(thread2, NULL);

// 销毁自旋锁

pthread_spin_destroy(&lock);

printf("Final value of shared_resource: %d\n", shared_resource);

return 0;

}

代码很简单,就是创建两个线程。这两个线程同时对共享变量shared_resource进行加一操作。为了保证互斥,我采用了pthread_spin_lock。

使用gdb进行调试

使用g++ test.cpp -lpthread -g进行编译生成可执行程序,注意要追加-g选项

采用gdb进行调试gdb ./a.out。在pthread_spin_lock(&lock);打断点,然后run到这里

采用set scheduler-locking on来禁止其他线程运行,这样一来就可以调试当前线程。

采用step单步调试进入pthread_spin_lock即可观察到其实现。

由于我的主机是x86-64架构的,所以对应的pthread_spin_lock的实现如下:

/* sysdeps/x86_64/nptl/pthread_spin_lock.S */

ENTRY(pthread_spin_lock)

1: LOCK ; 使用LOCK指令保持原子性和总线锁定状态

decl 0(%rdi) ; 对 %rdi 寄存器指向的地址的值减 1,即尝试获取自旋锁

jne 2f ; 若减 1 后的值不等于零(即锁已经被其他线程占用),跳转到标号 2 处继续执行

xor %eax, %eax ; %eax 寄存器清零,即返回值设为 0(表示成功获取锁),并准备返回

ret

.align 16 ; 将接下来的指令地址对齐到 16 字节边界,优化手段,提高性能

2: rep ; 自旋等待的开始,使用 rep 前缀,没有实际操作,只是为了占用一定的处理器时间

nop ; 自旋等待的一种方式,避免线程进入睡眠状态,减少上下文切换开销

cmpl $0, 0(%rdi) ; 检查锁是否已被释放,即比较 %rdi 寄存器指向的地址的值与零

jg 1b ; 若锁还未被释放(即 %rdi 寄存器指向的地址的值大于零),跳转回标号 1 处继续尝试获取锁

jmp 2b ;若锁已被释放,跳转回标号 2 处,继续自旋等待获取锁

END(pthread_spin_lock)

pthread_spin_unlock的实现如下:

/* sysdeps/x86_64/nptl/pthread_spin_unlock.S */

ENTRY(pthread_spin_unlock)

movl $1, (%rdi) ; %rdi 寄存器指向的地址的值置 1,即解锁

xorl %eax, %eax

retq

END(pthread_spin_unlock)

可以观察到,pthread_spin_lock仅仅采用的是自旋的方式,而没有采用preempt_disable来禁止抢占(用户态不允许调用)。

同时也观察到它使用label 2中的只读操作和rep nop来减少LOCK带来的性能损失。