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 <stdio.h>
#include <pthread.h>
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带来的性能损失。