Linux的库制作和原理

一、静态库(Static Library)

概念 :静态库在Linux下以 .a(Archive)为后缀。在编译链接阶段,链接器会将静态库中被调用的代码完整拷贝到最终的可执行文件中。因此,程序一旦编译完成,运行时便不再依赖原库文件。其优点是运行时无需额外依赖、执行速度快;缺点是生成的可执行文件体积较大,且库更新后需要重新编译所有依赖它的程序。

生成与使用示例

假设项目目录结构如下:

bash 复制代码
project/
├── include/
│   └── math_func.h
├── src/
│   └── math_func.c
└── main.c
  1. 生成静态库 :使用 gcc -c 将源文件编译为目标文件,然后使用 ar 命令将其打包为静态库。

    bash 复制代码
    gcc -c src/math_func.c -o math_func.o
    ar rcs libmath.a math_func.o
  2. 使用静态库:编译主程序时,需要用到三个关键参数:

    • -I:指定头文件的搜索路径(编译预处理阶段)
    • -L:指定库文件的搜索路径(链接阶段)
    • -l:指定要链接的库名 (省略 lib 前缀和 .a 后缀)
bash 复制代码
gcc main.c -I./include -L. -lmath -o main_static

二、动态库(Dynamic Library)

概念 :动态库(也称共享库)在 Linux 下以 .so(Shared Object)为后缀。在编译阶段,它仅将依赖关系记录在可执行文件中,并不拷贝代码;直到程序运行阶段,才由系统的动态链接器将库加载到内存。其优点是多个程序可共享同一份库内存镜像,节省磁盘和内存资源,且支持热更新;缺点是运行时必须确保库文件可用,否则程序无法启动。

生成与使用示例

同样基于上述项目结构:

  1. 生成动态库 :编译时必须添加 -fPIC 选项生成位置无关代码(Position Independent Code),以确保动态库可被加载到任意内存地址,随后使用 -shared 选项生成动态库

    bash 复制代码
    gcc -fPIC -c src/math_func.c -o math_func.o
    gcc -shared -o libmath.so math_func.o
  2. 使用动态库:编译主程序的命令与静态库完全一致:

bash 复制代码
gcc main.c -I./include -L. -lmath -o main_dynamic

动态库运行时找不到库的四种解决方法

编译顺利通过,但运行 ./main_dynamic 时却报错:

bash 复制代码
./main_dynamic: error while loading shared libraries: libmath.so:
cannot open shared object file: No such file or directory

原因-L 参数仅在编译链接阶段 告诉编译器去哪里找库,但程序运行阶段 由系统的动态链接器 ld-linux.so 负责加载动态库,它只会在以下固定路径中搜索:

  • 环境变量 LD_LIBRARY_PATH 指定的路径
  • 系统缓存 /etc/ld.so.cache 中记录的路径
  • 系统默认路径:/lib/lib64/usr/lib/usr/lib64

当前目录 . 不在上述任何路径中,因此运行时报错。下面介绍四种解决方法。


方法一:配置 /etc/ld.so.conf.d/(永久生效,生产环境推荐)

这是最规范的方式。通过向系统库配置目录中添加自定义配置文件,让动态链接器永久识别我们的库路径,且不污染系统目录。

bash 复制代码
# 1. 在 /etc/ld.so.conf.d/ 下创建配置文件(文件名自定义,后缀必须为 .conf)
sudo vim /etc/ld.so.conf.d/mylib.conf

# 2. 在文件中写入动态库所在的绝对路径,例如:
# /home/user/myproject/lib

# 3. 刷新系统库缓存(必须执行,否则配置不生效)
sudo ldconfig

# 4. 验证系统是否已识别该库
ldconfig -p | grep libmath
# 输出示例:libmath.so (libc6,x86-64) => /home/user/myproject/lib/libmath.so

# 5. 运行程序,成功!
./main_dynamic

优点 :永久生效、不污染系统目录、支持多版本库管理。

缺点:需要 root 权限。


方法二:拷贝动态库至系统默认路径(永久生效)

直接将动态库文件复制到系统默认的库搜索路径下(如 /usr/lib/lib64),简单粗暴。

bash 复制代码
# 将动态库拷贝到系统库目录(需要 root 权限)
sudo cp libmath.so /usr/lib/

# 刷新系统库缓存(可选,但建议执行)
sudo ldconfig

# 运行程序,成功!
./main_dynamic

优点 :操作简单,永久生效。

缺点:会污染系统目录;库更新后需要重新拷贝;不便于管理多个版本的库。


方法三:建立软链接到系统目录(永久生效)

与拷贝类似,但不复制文件本身,而是在系统库目录下创建一个指向原始库文件的符号链接(软链接),节省磁盘空间且方便更新。

bash 复制代码
# 在系统库目录下创建软链接(必须使用绝对路径)
sudo ln -s /home/user/myproject/lib/libmath.so /usr/lib/libmath.so

# 刷新系统库缓存
sudo ldconfig

# 运行程序,成功!
./main_dynamic

优点 :不占用额外磁盘空间,源文件更新后软链接自动生效。

缺点:需要 root 权限;如果源文件被移动或删除,软链接会失效(变成"断链")。


方法四:设置 LD_LIBRARY_PATH 环境变量(临时生效,开发调试推荐)

LD_LIBRARY_PATH 是动态链接器在搜索系统默认路径之前优先检查的环境变量。设置后仅在当前终端会话中生效,关闭终端即失效,适合开发阶段的快速测试。

bash 复制代码
# 临时生效(仅当前终端)
export LD_LIBRARY_PATH=/home/user/myproject/lib:$LD_LIBRARY_PATH
./main_dynamic   # 成功运行!

# 若希望永久生效(当前用户),可写入 ~/.bashrc
echo "export LD_LIBRARY_PATH=/home/user/myproject/lib:\$LD_LIBRARY_PATH" >> ~/.bashrc
source ~/.bashrc  # 刷新配置使其立即生效

优点 :无需 root 权限,设置简单,适合临时调试。

缺点export 方式仅对当前终端有效;写入 .bashrc 虽可持久化,但会影响该用户的所有程序,可能造成意外加载。

三、ELF文件

在Linux环境下开发C/C++程序时,我们经常会遇到.o目标文件、a.out可执行文件、.so共享库等文件格式。它们看似不同,但实际上都遵循同一种底层格式标准------ELF(Executable and Linkable Format)


1. ELF文件的四种类型

根据用途不同,ELF文件可以分为四类:

文件类型 文件后缀/示例 作用
可重定位文件 .o 包含代码和数据,用于和其他目标文件链接,生成可执行文件或共享库
可执行文件 a.out 包含可直接加载执行的程序
共享目标文件 .so 动态链接库,可在程序运行时被加载
核心转储文件 core dump 存放进程崩溃时的内存快照,用于事后调试

2. ELF文件的核心结构

复制代码
+------------------------+
|     ELF Header         |  <- 文件头,描述文件整体信息
+------------------------+
|  Program Header Table  |  <- 段表(执行视图),加载时使用
|       (可选)            |
+------------------------+
|                        |
|   Sections / Segments  |  <- 实际数据区
|                        |
+------------------------+
|  Section Header Table  |  <- 节表(链接视图),链接时使用
+------------------------+
2.1 ELF头(ELF Header)

ELF头位于文件最开头,描述了文件的整体信息,包括:文件类型、目标架构、入口点地址、程序头表和节头表的位置和大小等。

通过 readelf -h 命令可以查看:

bash 复制代码
$ readelf -h hello.o
ELF Header:
  Magic:   7f 45 4c 46 ...      # ELF魔数
  Class:   ELF64                # 64位架构
  Data:    2's complement, little endian
  Type:    REL (Relocatable file)  # 可重定位文件
  Machine: Advanced Micro Devices X86-64
  Entry point address: 0x0      # 目标文件无入口
  Start of program headers: 0   # 程序头表:无
  Start of section headers: 728 # 节头表:728字节处
  Number of section headers: 13

而对于可执行文件:

bash 复制代码
$ readelf -h a.out
  Type:    DYN (Shared object file)   # 位置无关可执行文件
  Entry point: 0x1060                 # 程序入口虚拟地址
  Start of program headers: 64        # 程序头表存在
  Number of program headers: 13

核心要点 :ELF头最重要的作用是定位文件的其他部分,它是解析整个文件的入口。

2.2 节头表(Section Header Table)------ 链接视图

节(Section)是ELF文件的基本组成单位,每个节包含特定类型的数据:

常见节名 描述
.text 代码节,保存程序机器指令
.data 数据节,保存已初始化的全局变量和静态变量
.rodata 只读数据节,保存常量字符串等
.bss 未初始化数据节,程序加载时由系统置零
.symtab 符号表,记录函数名、变量名与地址的映射

查看目标文件的节信息:

bash 复制代码
$ readelf -S hello.o
2.3 程序头表(Program Header Table)------ 执行视图

程序头表(也叫段表)告诉操作系统如何将文件内容加载到内存中。每个条目描述了一个段(Segment)的类型、偏移、虚拟地址、文件大小、内存大小和访问权限。

查看可执行文件的程序头:

bash 复制代码
$ readelf -l a.out

Program Headers:
  Type    Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flags Align
  PHDR    0x000040 0x400040   0x400040   0x1f8   0x1f8   RE    8
  INTERP  0x000238 0x400238   0x400238   0x1c    0x1c    R     1
  LOAD    0x000000 0x400000   0x400000   0x...   0x...   R E   0x1000
  LOAD    0x000e10 0x600e10   0x600e10   0x...   0x...   RW    0x1000
  DYNAMIC 0x000e28 0x600e28   0x600e28   0x1d0   0x1d0   RW    8
  ...

可以看到两个关键的LOAD段:

  • 第一个:R E(读+执行),包含.text等代码段

  • 第二个:RW(读+写),包含.data.bss等数据段


3. Section 合并为 Segment

3.1 链接时的合并

多个.o文件链接时,链接器会将相同属性的Section合并到一起:

bash 复制代码
code1.o        code2.o        code3.o
+------+      +------+      +------+        +------------------+
| .text |      | .text |      | .text |  ==> |      .text       |
+------+      +------+      +------+        +------------------+
| .data |      | .data |      | .data |  ==> |      .data       |
+------+      +------+      +------+        +------------------+
| ...   |      | ...   |      | ...   |      |       ...        |
+------+      +------+      +------+        +------------------+
3.2 合并原因:减少内存碎片,提高效率

操作系统管理内存的基本单位是页面(Page),通常是4KB。

假设不进行合并:

  • .text大小为512字节 → 占用1页(4KB),浪费3.5KB

  • .init大小为512字节 → 又占用1页,浪费3.5KB

  • 总共浪费7KB

如果合并后为1024字节 → 仅占用1页,节省了1页内存

本质:将多个小Section合并成大的Segment,使得内存分配更紧凑,减少页内碎片,提高内存使用效率。

3.3 权限控制

合并时,具有相同访问权限(如可读+可执行)的Section会被合并到同一个Segment。这样在加载时,OS只需为这个Segment统一设置权限,而不是逐节设置,简化了内存管理。

Section 权限 合并后的Segment
.text R+X LOAD (R+X)
.rodata R LOAD (R+X) 或 LOAD (R)
.data R+W LOAD (R+W)
.bss R+W LOAD (R+W)

4. 链接视图 vs 执行视图

ELF文件提供了两个视角:

视图 对应表 使用者 用途
链接视图 Section Header Table 链接器(ld) 按功能模块划分,进行符号解析和重定位
执行视图 Program Header Table 加载器(Loader) 告诉OS如何将文件段加载到内存,设置权限
bash 复制代码
                 +------------------+
                 |   ELF Header     |
                 +------------------+
                 | Program Header   |  <- 执行视图(Loader使用)
                 |    Table         |
                 +------------------+
                 |                  |
                 |   Segments       |
                 |                  |
                 +------------------+
                 | Section Header   |  <- 链接视图(Linker使用)
                 |    Table         |
                 +------------------+

四、理解连接和加载

1. 链接的"胶水":重定位与符号解析

假设我们有两个文件:main.cutils.c

cpp 复制代码
// utils.c
int hidden = 42;
int add(int a, int b) {
    return a + b;
}


// main.c
extern int hidden;
int add(int, int);

int main() {
    int sum = add(1, 2);
    return hidden + sum;
}

当我们分别编译它们(gcc -c)后,得到了 main.outils.o。此时,main.o 中并不知道 add 函数和 hidden 变量存在于何处。

链接器(ld) 的工作就是收集 所有 .o 文件,进行符号解析 (找到每个符号的定义),然后进行重定位(把代码中的占位符替换为真实的内存地址)。

在静态链接中,链接器会把所有 .o 中的代码段 (.text)数据段 (.data) 合并起来,形成一个大文件。这一步不仅仅是机械地拼接,链接器还会计算出每个符号在合并后的新文件中的最终运行时虚拟地址 (比如在 Linux 下,通常从 0x400000 开始)。

2. 为什么反汇编 .o 时,call 地址是 0?

这是理解链接过程的关键一步。让我们反汇编 main.o

cpp 复制代码
$ objdump -d main.o

0000000000000000 <main>:
   0:   f3 0f 1e fa             endbr64 
   4:   48 83 ec 08             sub    $0x8,%rsp
   8:   be 02 00 00 00          mov    $0x2,%esi
   d:   bf 01 00 00 00          mov    $0x1,%edi
  12:   e8 00 00 00 00          callq  17 <main+0x17>  ; 这里的地址是 0!
  17:   89 c2                   mov    %eax,%edx
  19:   8b 05 00 00 00 00       mov    0x0(%rip),%eax  ; 这里的地址也是 0!
  1f:   01 d0                   add    %edx,%eax
  21:   48 83 c4 08             add    $0x8,%rsp
  25:   c3                      retq

你会发现:

  • callq 17 后面的机器码是 e8 00 00 00 00e8call 的操作码,后面的 00 00 00 00偏移量占位符

  • mov 0x0(%rip), %eax 后面的偏移也是 00 00 00 00

原因在于 :编译阶段,编译器并不知道 add 函数在最终内存中的绝对地址,也不知道 hidden 变量相对于当前指令(RIP)的距离。因为此时还未进行合并和地址分配,所以编译器暂时用 0 填充 ,并在 .rela.text 段中记录一条重定位条目,告诉链接器:"这里需要修正,请把正确的地址填进来"。

3. 地址是如何被修正的?

当我们进行静态链接后(gcc main.o utils.o -o prog),再反汇编可执行文件 prog

cpp 复制代码
$ objdump -d prog

0000000000401126 <main>:
  401126:   f3 0f 1e fa             endbr64 
  40112a:   48 83 ec 08             sub    $0x8,%rsp
  40112e:   be 02 00 00 00          mov    $0x2,%esi
  401133:   bf 01 00 00 00          mov    $0x1,%edi
  401138:   e8 13 00 00 00          callq  401150 <add>  ; 被修正为 0x13!
  40113d:   89 c2                   mov    %eax,%edx
  40113f:   8b 05 c3 2e 00 00       mov    0x2ec3(%rip),%eax  ; 被修正为 0x2ec3!
  401145:   01 d0                   add    %edx,%eax
  401147:   48 83 c4 08             add    $0x8,%rsp
  40114b:   c3                      retq

链接器做了两件关键的事:

(1)对于 call 指令(相对跳转)

call 的机器码 e8 后面跟的是相对偏移

  • 链接器找到了 add 的最终地址:0x401150

  • call 指令本身的地址是 0x401138,该指令长度为 5 字节。

  • 公式:目标地址 - (当前地址 + 指令长度) = 0x401150 - (0x401138 + 5) = 0x401150 - 0x40113D = 0x13

  • 所以,机器码从 e8 00 00 00 00 变成了 e8 13 00 00 00。CPU 在运行到这一行时,会用 RIP + 0x13 跳转到 add 函数。

(2)对于 mov hidden(RIP 相对寻址)

hidden 是一个全局变量,存放在 .data 段。

  • 链接器计算得出 hidden 的绝对地址在 0x404010

  • 当前指令 mov 的地址是 0x40113f,指令长度 6 字节,所以 RIP 在运行时指向 0x401145

  • 公式:目标地址 - 当前RIP = 0x404010 - 0x401145 = 0x2ECB(这里机器码显示为 2ec3 是因为小端序和其他对齐微调,但原理完全一致)。

4. 总结:静态链接的本质

静态链接的过程,本质上就是将分散在多个 .o 文件中的零散逻辑,通过符号表映射,聚合成一个统一的线性地址空间

  • .o 中看到 0 :是因为此时处于未分配地址状态,编译器只负责生成骨架,把寻址工作留给了链接器(通过重定位表)。

  • 在可执行文件中看到具体值 :是链接器完成了地址绑定,将所有符号引用替换为最终内存布局中的实际地址(绝对地址或基于 RIP 的偏移)


五、理解链接与加载

1. ELF未加载时就有地址

很多人直觉上认为,程序只有被加载到内存后才有地址。但事实恰恰相反------ELF文件在编译阶段就已经完成了统一编址

我们用 objdump -S 反汇编一个程序,最左侧那列就是ELF的虚拟地址(严格说是逻辑地址,起始地址+偏移量,但我们视起始地址为0):

cpp 复制代码
0000000000001149 <run>:
    1149:       f3 0f 1e fa             endbr64
    114d:       55                      push   %rbp
    114e:       48 89 e5                mov    %rsp,%rbp
    1151:       48 8d 3d ac 0e 00 00    lea    0xeac(%rip),%rdi  # 2004 <_IO_stdin_used+0x4>
    1158:       e8 f3 fe ff ff          callq  1050 <puts@plt>
    ...

0000000000001163 <main>:
    1163:       f3 0f 1e fa             endbr64
    1164:       55                      push   %rbp
    1165:       48 89 e5                mov    %rsp,%rbp
    1168:       48 8d 3d a0 0e 00 00    lea    0xea0(%rip),%rdi  # 200f <_IO_stdin_used+0xf>
    116f:       e8 e2 fe ff ff          callq  1056 <puts@plt>
    1174:       b8 00 00 00 00          mov    $0x0,%eax
    1179:       e8 cb ff ff ff          callq  1149 <run>
    ...

这些地址在编译链接阶段 就已经确定下来了。当代计算机采用平坦模式工作,要求ELF对自己的代码和数据进行统一编址,这为后续的加载执行奠定了基础。


2. mm_struct 和 vm_area_struct 初始化数据从哪来?

当进程刚刚创建,内核需要为它初始化内存描述符 mm_struct 和虚存区域 vm_area_struct。这些数据不是凭空产生的,而是从ELF文件的各个Segment中解析出来的

ELF Segment mm_struct / vm_area_struct
每个Segment的起始地址 vm_area_struct->vm_start
每个Segment的长度 vm_area_struct->vm_end
段的权限(读/写/执行) vm_area_struct->vm_flags
Entry point地址 mm_struct->start_code

此外,这些详细地址还会用来填充页表,建立虚拟地址到物理地址的映射关系。


3. ELF Header 中的入口点

编译完成后,程序的入口地址会被记录在ELF Header的 Entry point address 字段中:

cpp 复制代码
$ readelf -h a.out
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x1060          ← 入口点!
  Start of program headers:          64 (bytes into file)
  Start of section headers:          14768 (bytes into file)
  Number of program headers:         13
  Number of section headers:         31

这个入口点地址,就是进程启动后CPU第一条指令所在的虚拟地址。

CPU怎么知道你的可执行程序的起始地址?EIP (指令指针寄存器) :存放了 下一条要执行的指令地址 。当进程启动时,操作系统会将 ELF 头中的入口点地址 0x1060 写入 EIP。


4. 一张图看懂ELF加载与进程地址空间

cpp 复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                         ELF 文件 (磁盘)                            │
├─────────────────────────────────────────────────────────────────────┤
│  ELF Header  │  Program Headers  │  Segments (代码/数据/RODATA)   │
│              │  (Segment信息)     │  已包含虚拟地址!               │
│ Entry: 0x1060│  起始地址、长度、权限  │                              │
└───────────────────────────┬─────────────────────────────────────────┘
                            │ 加载时解析
                            ▼
┌─────────────────────────────────────────────────────────────────────┐
│                        内核加载器                                   │
│  1. 读取ELF Header,获取Entry point                                │
│  2. 遍历Program Headers,提取每个Segment的起始地址、长度、权限       │
│  3. 分配物理页框,建立页表映射                                      │
│  4. 初始化mm_struct / vm_area_struct                               │
└───────────────────────────┬─────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────────────┐
│                    进程虚拟地址空间                                  │
├─────────────────────────────────────────────────────────────────────┤
│  ┌──────────────────────────────────────────────────────────────┐   │
│  │  mm_struct                                                 │   │
│  │  ├─ start_code, end_code  ← 从ELF的代码段解析              │   │
│  │  ├─ start_data, end_data  ← 从ELF的数据段解析              │   │
│  │  ├─ start_brk, brk        ← 堆的起始和边界                 │   │
│  │  └─ mmap_base             ← 内存映射基址                   │   │
│  └──────────────────────────────────────────────────────────────┘   │
│                                                                     │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐    │
│  │ 代码段  │ │ 数据段  │ │ RODATA  │ │  堆     │ │  栈     │    │
│  │ (RX)    │ │ (RW)    │ │ (RO)    │ │ (RW)    │ │ (RW)    │    │
│  │vm_start │ │vm_start │ │vm_start │ │         │ │         │    │
│  │  ↓      │ │  ↓      │ │  ↓      │ │         │ │         │    │
│  │vm_end   │ │vm_end   │ │vm_end   │ │         │ │         │    │
│  └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘    │
│       ↑            ↑            ↑                                  │
│       └────────────┴────────────┘                                  │
│              每个vm_area_struct的start/end                          │
│              都来自ELF的Segment信息                                 │
└─────────────────────────────────────────────────────────────────────┘

5. 从磁盘到内存:进程运行的完整流程

第一步:磁盘上的可执行程序

ELF文件存放在磁盘中,其中的代码和数据已经包含了虚拟地址(编译链接时确定)。例如:

虚拟地址 指令内容
0x1000 f3 0f 1e fa (endbr64)
0x1004 31 ed (xor %ebp,%ebp)
0x1006 49 89 d1 (mov %rdx,%r9)
... ...

这些地址是逻辑地址,并非物理内存的真实位置。


第二步:加载器解析ELF并初始化进程

当执行 ./a.out 时,内核的加载器会:

  1. 读取ELF Header ,获取入口点地址(如 0x1060

  2. 遍历Program Headers,获取每个Segment的起始虚拟地址、长度和权限

  3. 创建进程的 mm_struct,用Segment信息填充各区域(代码段、数据段等)

  4. 为每个Segment创建 vm_area_struct ,记录 vm_startvm_end 和权限


第三步:建立页表------虚拟地址到物理地址的桥梁

此时,虚拟地址已经确定(如 0x1000),但物理内存尚未分配 。加载器通过按需分页(Demand Paging) 机制:

  • 在页表中为虚拟地址 0x1000 创建条目,但暂时不分配物理页框

  • 将页表项标记为缺页状态(Present位 = 0)

  • 当CPU真正执行到该地址时,会触发缺页异常(Page Fault)


第四步:缺页异常处理------真正加载到物理内存

当程序开始执行,CPU访问虚拟地址 0x1000 时:

  1. MMU查页表,发现该页不在物理内存中 → 触发缺页异常

  2. 内核的缺页处理程序被调用:

    • 从磁盘的ELF文件中读取对应的页面(如从文件偏移量 0x1000 处读取一页)

    • 在物理内存中分配一个空闲页框 (如物理地址 0x0000001000000000

    • 将磁盘内容拷贝到该物理页框

    • 更新页表 :将虚拟地址 0x1000 映射到物理地址 0x0000001000000000

    • 标记页表项为有效(Present = 1)

  3. CPU重新执行引起缺页的指令,此时访问命中物理内存


第五步:正常执行------通过页表完成地址转换

之后,CPU每次访问虚拟地址时:

  • MMU自动查询页表,将虚拟地址转换为物理地址

  • 从物理内存中取指/取数

  • 进程正常运行

相关推荐
技术民工之路2 小时前
RAG 在真实业务项目落地全流程(从需求到上线运维)
运维
极客先躯2 小时前
高级java每日一道面试题-2026年04月05日-实战篇[Docker]-如何排查容器 OOM 的问题?
java·运维·docker·容器·内存溢出·高级面试
非鱼䲆鱻䲜2 小时前
CCS 20.5.0的printf重定向串口怎么实现?
linux·单片机·电赛·ccs
左手厨刀右手茼蒿2 小时前
PCIe设备驱动开发:从枚举到DMA传输的完整链路
linux·嵌入式·系统内核
济6172 小时前
I.MX6U Linux 驱动开发篇---零基础必看!中断实验(按键中断 + 定时器消抖 + 设备树配置实战教程)--- Ubuntu20.04
linux·驱动开发·嵌入式·嵌入式linux驱动开发
玖玥拾2 小时前
C# 语言进阶(十四)Unity UI界面开发 + 网络消息联动
服务器·开发语言·网络·ui·unity·c#·游戏引擎
seven_stars_3 小时前
CVE-2018-19518PHP漏洞
linux·浏览器·kali
浅止菌3 小时前
嵌入式设备ping不通?tcpdump 5分钟找到凶手,比加printf快10倍
linux
Amonter3 小时前
TB-RK3399Pro移植ubuntu 26.04及烧录
linux·ubuntu·rockchip·rk3399pro