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库的动态连接库路径

注:

  1. 如果configure失败,记得执行make distclean或者make clean清理配置信息;
  2. 我在编译时并没有一次编译成功,期间遇到了一些报错。这些报错主要是因为编译器将一些warning视为了error,这时只需添加编译选项-Wno-error=[类型名称],比如-Wno-error=attribute-alias,-Wno-error=maybe-uninitialized。这样一来,只需重新执行make CFLAGS="-g -Og -Wno-error=maybe-uninitialized -Wno-error=maybe-uninitialized"即可。
  3. --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进行调试

  1. 使用g++ test.cpp -lpthread -g进行编译生成可执行程序,注意要追加-g选项
  2. 采用gdb进行调试gdb ./a.out。在pthread_spin_lock(&lock);打断点,然后run到这里
  3. 采用set scheduler-locking on来禁止其他线程运行,这样一来就可以调试当前线程。
  4. 采用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)
  1. 可以观察到,pthread_spin_lock仅仅采用的是自旋的方式,而没有采用preempt_disable来禁止抢占(用户态不允许调用)。
  2. 同时也观察到它使用label 2中的只读操作和rep nop来减少LOCK带来的性能损失。
posted @ 2023-07-24 23:20  深海·蓝河  阅读(4756)  评论(0编辑  收藏  举报