1 Star 0 Fork 11

coder_lw / wiki

forked from deepinwiki / wiki 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
c和c++编译.md 20.10 KB
一键复制 编辑 原始数据 按行查看 历史

c 和c++ 编译

前言

c / c++ 是 linux 中常见的编译语言。虽然我们不是开发人员,但是作为 linux 用户,经常要自己从源代码构建可用的应用程序,所以我们也需要理解 c / c++ 的源代码是怎么编译成可以执行的应用程序的。

编译过程

基本过程:c / c++ 源代码.c --> 预编译 --> 汇编.s --> 目标文件.o --> 可执行文件

库: 一个或者多个目标文件可以组成库。分静态库 xxx.a 和动态库 xxx.so 两种。

程序: 程序是多个目标文件、静态库、动态库组合而成的可执行单元。

链接:多个目标文件组成库或程序的过程,叫链接。也可以细为动态链接 ld,静态链接 ar。

动态库: 动态库并不合并到程序中,而是在运行时从指定磁盘路径装载对应的动态库。这可以让多个程序共享一个动态库,但是也会面临丢失动态库文件,或者动态库版本不对,而导致程序无法运行的问题。这个被俗称为 dll 地狱,是一种常见的应用失败原因。

通过 clang 演示编译过程的例子:

# 预处理
# test.c 是 c 语言的源文件
# -E 表示只进行到预处理阶段 
# -o 表示输出
clang -E test.c -o test.i
# 编译成汇编源文件
clang -S test.i -o test.s
# 汇编
clang -c test.s -o test.o
# 链接
clang test.o -o test

clang 或者 gcc 是一个集成一站式的编译工具,它内部实际会调用相关的工具。可以手动指定工具,这在一些特殊场合是有必要的。

其中,可能会用得比较多的是链接器这一部分。链接是比较复杂的,它有静态链接,动态链接,而动态链接在运行时还会影响到最终用户。

接下来的内容重点也放在这一部分。

标准库

c 语言的标准库是 libc,所谓标准库,就是这个语言编译的每一个程序,几乎都会用到的一组公共功能。而 libc 并不是只有一种,有不同的厂家推出不同的 libc,其中最常用的是 gcc 编译器自带的 libc.so.6 (glibc)。

不过 glibc 有些臃肿,就有人选择 libc.so (musl)。

链接器种类: ld, lld, gold

  1. 运行时
    1. 启动:crt1.o (_start 入口),Scrt1.o 是 pie 版本
    2. 初始化:crti.o (.init /.fini 段)
      1. 结束:crtn.o (结尾)
    3. 构造:crtbegin.o (构造函数 c++)
      1. 析构:crtend.o
  2. 静态标准库: libc.a
  3. 动态标准库: libc.so / libc.so.6
  4. 动态链接器: ld-linux.so.2 / ld-linux-x86_64.so.2 / ld-musl-x86_64.so.1

动态链接因为并不是直接整合到程序里面的,而是在程序运行时加载的,那么这个加载器就叫动态链接器。

运行时是一个 c 程序启动和关闭需要的基本功能。如果使用 gcc 或 clang 来编译,他会自动添加这些部分。如果是自己使用链接器,就需要自己添加他们。

musl 动态链接使用演示:

# musl 手动链接
# test.o 目标文件
# test 最终程序
# --dynamic-linker 指定动态链接器,因为是musl,所以选择 ld-musl-x86_64.so.1
# -L 搜索的目录,执行为 musl 的库路径 /usr/lib/musl/lib
# -lc 表示链接指定库,会自动修改为 libxxx.so,如果找不到会找 libxxx.a。所以这里就是 libc.so。如果想静态链接,那么可以改成 -l:libc.a
# -l:crt1.o 表示链接 crt1.o,这个形式会严格按照文件名链接
ld.lld test.o -o test -dynamic-linker /lib/ld-musl-x86_64.so.1 -L /usr/lib/musl/lib -lc -l:crt1.o -l:crti.o -l:crtn.o

# musl-clang 集成编译系统
# -static 表示静态链接所有库
musl-clang test.o -static 

# zig cc 编译器
# 动态链接到 musl-libc
zig cc test.o -o test -Wl,-dynamic-linker=/lib/ld-musl-x86_64.so.1 -L /usr/lib/musl/lib
# 静态链接到 musl-libc
zig cc test.o -o test -nostdlib -Wl,-Bstatic -l:crt1.o -l:crti.o  -L /usr/lib/musl/lib -lc -l:crtn.o
# 使用 zig 内置的 musl-libc
zig cc  test.o -o test -target x86_64-linux-musl

c++ 语言的标准库是 libstdc++(gcc)或 libc++(clang)。

  1. 运行时
    1. crtbegin.o
      1. crtbeginS.o (PIE 位置无关程序)
      2. crtbeginT.o (静态链接时)
    2. crtend.o
      1. crtendS.o
    3. libgcc.a / libgcc_s.so.1
    4. libgcc_eh.a

推荐链接顺序: crt1.o crti.o crtbegin.o [-L 搜索路径] [目标文件] [gcc libs] [C libs] [gcc libs] crtend.o crtn.o

注意, gcc 的 libstdc++ 库虽然支持静态链接,但是它和 gcc 的 libc 是强制依赖的。所以,使用 libstdc++ 时不要替换 glibc,只能联合使用。

另外, gcc 对静态链接 c++ 标准库的支持不好,请使用 g++。

演示:

# test.o 是 c 语言目标文件
# test2.o 是 c++ 语言目标文件
# -static 静态链接标准库
g++ test.o test2.o -static

# libstd++ / libgcc 是静态链接,而 libc 是动态链接的
g++ test.o test2.o -static-libstd++ -static-libgcc

# 效果同上
# -pie 位置无关程序(一种安全措施)
# Scrt1.o 是 PIE 版 crt1.o
# -Bstatic 表示往后静态链接
# -Bdynamic 表示往后优先动态链接 
ld.lld -pie -l:Scrt1.o -l:crti.o -l:crtbeginS.o -L /usr/lib/gcc/x86_64-pc-linux-gnu/12.1.0/ -L /usr/lib test.o test2.o -Bstatic -lstdc++ -lgcc -lgcc_eh -Bdynamic -lc -lm -Bstatic -lgcc -lgcc_eh -l:crtendS.o -l:crtn.o --dynamic-linker /lib/ld-linux-x86-64.so.2 

ldd a.out
# 显示:
linux-vdso.so.1 (0x00007ffec59ba000)
      libc.so.6 => /usr/lib/libc.so.6 (0x00007f8d38600000)
      /lib/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f8d38875000)
      libm.so.6 => /usr/lib/libm.so.6 (0x00007f8d38519000)

也许你注意到推荐链接里面有个重复的地方,这是因为有些库会相互依赖。也就是 a 依赖 b,但 b 也会依赖 a,这时候,链接就会发生找不到定义。

要解决这个问题,可以添加一个搜索组,组成员会循环的解决内部的依赖关系,而不是默认的按顺序判断。

# -static 静态链接
# -nostdlib 不要自动链接标准库
# -Wl,--star-group 传递 --start-group 参数给链接器,创建一个搜索组
# -Wl,--end-group 组结束标志
gcc  -l:crt1.o -l:crti.o -l:crtbegin.o test.o test2.o -static -nostdlib -lstdc++ -Wl,--start-group -lgcc -lgcc_eh -lc -Wl,--end-group -l:crtend.o -l:crtn.o

为什么要用静态链接,因为程序分 pie 和非 pie 和静态版本,它和 libc 是强相关的。如果 libc 是静态链接,其余部分也必须如此配合。而其他库就没有这个限制。如可以在动态程序里面支持静态链接 libstdc++.a 。

clang 编译参数

其他可能有用的 clang 编译参数:

  1. -nostdinc : 不使用系统标准库的头文件
    1. -isystem: 指定系统级头文件路径
    2. -I: 头文件路径
    3. -L: 库搜索路径
    4. -Wl,-rpath: 将搜索路径嵌入动态库中
  2. -nostdinc++ : 不使用系统 c++ 标准库的头文件
  3. -nostdlib : 不使用系统自带的标准库
  4. -nostdlib++: 不使用系统自带的 c++ 标准库
  5. --sysroot: 设定搜索的相对根目录
    1. -isysroot: 头文件根目录(同时以上选项变为只搜索库)
  6. -nodefaultlibs : 不使用系统默认库
  7. -nostartfiles : 不链接运行时(crtxxx)
  8. -fuse-ld=lld : 将默认的 ld 链接器改为 lld (ld.lld)
  9. --rtlib=compiler-rt / libgcc : 修改运行时
  10. --stdlib=libc++ / libstdc++ : 修改 c++ 标准库
  11. -Wl,--as-needed : 只链接需要的动态库
  12. -Wl,--dynamic-linker=/lib/ld-musl-x86_64.so.1 : 设置动态链接器

musl

glibc 虽然占据主流地位,但是太臃肿,它的大小差不多6m,而 musl libc 才不到 3m。尤其在一些 docker,嵌入式等平台,就很需要更精简的 c 库了。

为此,我们可能需要做两种情况的考虑。一个是编译为本机代码,只是替换为 musl libc。这种情况我们需要考虑的是,怎么不被当前本机环境影响,比如我们有些库可能会依赖 glibc,或者不小心引入了 glibc 版本的库。我们需要提供一个纯净的 musl libc 编译环境。

另一种情况我们要编译成别的平台的代码,这叫交叉编译,我们需要一个运行在本地,但是能产生目标平台代码的交叉编译环境。这比上一种情况更加复杂。

传统上,我们使用 gcc 或者 clang 这种工具链,需要配置复杂的交叉平台环境,但是现在有一个替代品,她就是 zig 语言编译器。这个编译器能支持 c 和 c++ 的编译,并且提供了相关目标平台的基本库(这在传统上是非常复杂,且容易出错的)。

目标平台三元组(target triple):

  1. 一般由四部分组成:指令架构 - 供应商 - 系统 - 库标准
  2. arch 指令架构:i386, x86_64, riscv64 等
  3. vendor 供应商:pc, unknown(未知)等
  4. os 系统:linux,windows 等
  5. env 库标准(一般是 c 库提供商的标准): gnu(glibc),musl 等

不过三元组的名字,并没有严格的统一标准,各个编译器采用的也许不一样。它主要是给编译器一个参考,其中最重要是指令架构和库标准,它决定了编译器会编译成那种指令,和链接哪种标准库。

# zig cc 和 clang 的用法一致
zig cc --version

# zig c++ 同时支持 c++ 和 c
# -target 指定目标平台。如: riscv64-linux-musl
# zig 的 musl 目标,会静态链接
zig c++ test.c test2.cpp -target riscv64-linux-musl

file a.out
# 显示:
a.out: ELF 64-bit LSB executable, UCB RISC-V, RVC, double-float ABI, version 1 (SYSV), statically linked, not stripped

# 用 qemu 虚拟机运行程序,ok
qemu-riscv64 a.out

动态链接

musl 一般是用于静态链接。但是也可以用做动态链接。这时候,它和 glibc 有一些不同。

配置搜索路径: /etc/ld-musl-x86_64.patch

工作原理: 首先使用 musl 编译的程序,会指定链接器: /lib/ld-musl-x86_64.so.1 。 这并不是一个一般的库,而是一个可执行程序,它就是负责加载程序需要的动态链接库的链接器。

那么它从哪里加载相关库呢?首先它读取程序文件中记录的相关动态库的名称(soname),但它并不包括路径。所以这就需要另外给出路径。一般在编译的时候,会添加一些路径到程序内部(-rpath 选项)。最终用户还能通过上述配置文件,自己添加路径。

如果只需要临时添加搜索路径,那么可以通过环境变量: LD_LIBRARY_PATH 指定。

# 路径以冒号分隔
LD_LIBRARY_PATH=/lib:/usr/lib ./a.out

为什么动态链接会经常出现问题。因为它将出错的时机留到了安装到最终用户电脑上,用户配置错误的路径。而且,不同链接器其实是不兼容的。用 musl 开发的库,用 glibc 的默认加载器就会出现错误。反之亦然。

新的新版本和旧版本未必是百分百兼容的。

因此,设计者就使用了不同命名来缓解这个问题。一个动态库包括三个名字:真实名(realname)、动态名(soname)、链接名(linkname)。

  1. realname : 即输出的文件名,按格式为 libxxx.1.0。 库名基本上是 lib 开始。链接参数只需要 -lxxx 就会实际加载 libxxx.so。 后面 1.0 是自定义的版本号。如果升级了就应该换个版本号,避免兼容性问题。
  2. soname: 嵌入程序和库中的动态库名称。这个名称比上一个更加稳定,上一个可以修补,然后升级小版本号,如 realname 修改为: libxxx.1.9。而 soname 一般兼容时都不会改变。例: libxxx.so.1。它实际也是链接到 realname 文件上。mu
  3. linkname: 为了方便使用,它一般链接到 realname 文件上。如 libxxx.so 。这时,链接参数只需要给出 -lxxx 即可。
  4. 编译时:-lxxx --> libxxx.so --> libxxx.so.1.1 --> 获取 soname libxxx.so.1 记录到程序里面。
  5. 运行时: 在程序文件中获取动态加载器的路径 /lib/ld-musl-x86_64.so.1 执行它 --> 读取程序中记录的 soname libxxx.so.1 --> 根据动态加载器配置的路径,缓存 soname 和 soname 文件路径的对应关系 --> 从链接文件中获取 realname libxxx.so.1.0 并加载。

总结就是 soname 是动态加载器使用的链接文件。编译器会将这个名字写入库文件内部,和链接到它的所有程序和库内部。

linkname 只是为了编译参数方便和不用变更所使用的链接文件。

realname 是实际的库文件。三者配合下,库文件可以比较灵活的变更。

构建基于 musl 的编译系统(探索中~)

musl 只是一个 libc 库,这对于只用 c 库的情况是足够的,但是,linux 中大多数项目都用了 c++。 虽然 c++ 本身也会引用到 c 库,但是 gnu gcc 的 c++ 库是绑定自己的 glibc 库的,所以并不能简单混用。

因此,我们只能重新构建一个依赖 musl 的 c++ 库,这时候就面临选择,选择 libstd++ (gnu gcc) 还是 libc++ (llvm clang)。事实 libstd++ 对 glibc 的依赖过于紧密,所以选择 libc++。

libstdc++ 依赖 libgcc 运行时库。libc++ 对应的产物是 compiler-rt 。llvm 项目将这一部分分得更细, libc++ 可以选择 abi(二进制接口)的标准,它可以依赖 libc++abi 库(一个相对libstdc++abi 更加精细的 abi),同时将栈回溯技术分离成 libunwind 库。

默认情况,因为 gcc 是 linux 系统事实上的标准,clang 为了兼容,它默认是依赖 glibc 和 libstdc++ (libgcc)。因为 libgcc 是一个运行时,也就是 clang 编译出来的程序,都会带运行时。这就导致对 libgcc 有紧密的依赖。如果我们用 clang 来构建新的 compiler-rt 运行时,它也会依赖,这就是一个死循环。

因此,我们首先要创建一个不依赖 libgcc 的 clang, 然后用它构建 compiler-rt ,然后用这些和 musl 构建 libc++ libc++abi libunwind。最后,再用新的库构建全新的 clang,这就完成了一套基于 musl 的编译系统。

下载 llvm 项目

下面是编译方法。

llvm 是 clang 的母项目,它包含 libc++ 等组件。LLVM 采用 cmake 构建系统。

cmake 基本用法:

  1. -B build: 在当前目录下创建 build 子目录,这是构建系统的工作目录,可以删除来重新生成新的构建环境。
    1. rm -rf build : 直接删除构建目录即可
  2. -Dxxx=yyy: 其中 xxx 是当前项目可识别的参数,不同项目这个有所不同。 yyy是设置它的值。
  3. -G Ninja: cmake 是个高级构建工具,它会生成下级构建系统需要的构建文件,Ninja 是常见的下级构建系统,速度很快。
    1. Ninja -C build : 将构建 build 目录的所有目标
    2. Ninja -C build clean: 清理
  4. -L[A/H]: 列出当前项目配置的参数
# 下载 llvm 的镜像源
# 
# 镜像源将加速我们下载的速度
git clone -depth=1 https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project.git

# 进入 llvm 目录
cd llvm-project

半成品 clang

# 配置项目
cmake -B build -G Ninja -S llvm \
-DLLVM_ENABLE_PROJECTS=clang \
-DLLVM_USE_LINKER=lld \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-DCMAKE_BUILD_TYPE=Release \
-DCLANG_DEFAULT_RTLIB=compiler-rt \
-DCLANG_DEFAULT_CXX_STDLIB=libc++ \
-DCLANG_DEFAULT_LINKER=ld.lld \
-DCLANG_DEFAULT_UNWINDLIB="" \
-DLLVM_TARGETS_TO_BUILD="X86;RISCV" \
-DLLVM_DEFAULT_TARGET_TRIPLE="x86_64-unknown-linux-musl"



-DLLVM_TARGET_ARCH="native-unknown-linux-musl" \
-DCMAKE_C_FLAGS="" \
-DCMAKE_CXX_FLAGS=""  \
-DLLVM_TARGETS_TO_BUILD="X86;RISCV" \
-DLIBCXX_HAS_MUSL_LIBC=yes \
-DLIBCXX_USE_COMPILER_RT=yes \
-DLIBCXX_CXX_ABI=libcxxabi \
-DLIBCXX_HAS_PTHREAD_API=yes \
-DLIBCXXABI_USE_LLVM_UNWINDER=yes \
-DLIBCXXABI_USE_COMPILER_RT=yes \
-DLIBUNWIND_USE_COMPILER_RT=yes \
-DCOMPILER_RT_DEFAULT_TARGET_TRIPLE=native-unknown-linux-musl \
-DCOMPILER_RT_USE_LLVM_UNWINDER=ON \
-DCOMPILER_RT_USE_BUILTINS_LIBRARY=ON

-DLLVM_ENABLE_EH=yes \
-DLLVM_ENABLE_RTTI=yes

# 构建
ninja -C build
# 安装到指定目录
DESTDIR=$(realpath ../llvm-root) fakeroot ninja -C build install

libc++

libc++ 是 clang 自带的 c++ 标准库,它的好处是比较独立,规范。libstdc++ 是 gnu gcc 的一部分,它和其余整合比较严密,所以为了配置 musl c 库来使用,最好用 libc++ 替换 libstdc++。

zig 编译器虽然能够构建出静态版本,但是如果我们想动态链接怎么办?这时候也许就需要我们自己去编译目标平台的 libc++ 库了。

# 配置项目
cmake -G Ninja -S runtimes -B build \
-DLLVM_ENABLE_RUNTIMES="libcxx;libcxxabi;libunwind;" \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-DCMAKE_C_FLAGS="--sysroot=/lib/musl -rtlib=compiler-rt -Wl,--as-needed" \
-DCMAKE_CXX_FLAGS="--sysroot=/lib/musl -rtlib=compiler-rt -Wl,--as-needed"  \
-DCMAKE_BUILD_TYPE=Release \
-DLLVM_TARGETS_TO_BUILD="X86;RISCV" \
-DLLVM_DEFAULT_TARGET_TRIPLE=x86_64-unknown-linux-musl \
-DLIBCXX_HAS_MUSL_LIBC=YES \
-DLIBCXX_USE_COMPILER_RT=YES \
-DLIBCXX_CXX_ABI=libcxxabi \
-DLIBCXX_HAS_PTHREAD_API=YES \
-DLIBCXXABI_USE_LLVM_UNWINDER=YES \
-DLIBCXXABI_USE_COMPILER_RT=YES \
-DLIBUNWIND_USE_COMPILER_RT=YES

# 构建
ninja -C build
# 安装到指定目录
DESTDIR=$(realpath ../llvm-root) fakeroot ninja -C build install

-Wl,--dynamic-linker=/lib/ld-musl-x86_64.so.1
-DLLVM_USE_LINKER=ld \
--sysroot=$(realpath ../llvm-root/usr/local) 
$(realpath ../llvm-root/usr/local/bin/clang)
-Wl,-dynamic-linker=/lib/ld-musl-x86_64.so.1

compiler-rt

修补 musl 没有 execinfo.h 错误

musl 作为编译目标没有 execinfo.h 文件,而编译 compiler-rt 需要这个。apline 系统中又一个 libexecinfo 库,借来用用。

项目地址: https://git.alpinelinux.org/aports/tree/main/libexecinfo?h=3.10-stable

# 下载相关文件(略)

# 解压缩
tar -xvf libexecinfo-1.1.tar.bz2

# 打补丁
cd libexecinfo-1.1
patch --verbose -p1 < ../10-execinfo.patch
patch --verbose -p1 < ../20-define-gnu-source.patch
patch --verbose -p1 < ../30-linux-makefile.patch

# 编译
# 动态库
musl-clang  -shared execinfo.c stacktraverse.c -fuse-ld=lld -rtlib=compiler-rt --sysroot /lib/musl -Wl,-soname,libexecinfo.so.1 -o libexecinfo.so.1
ln -s libexecinfo.so.1 libexecinfo.so

# 静态库
musl-clang -c execinfo.c stacktraverse.c --sysroot /lib/musl 
llvm-ar rcs libexecinfo.a execinfo.o stacktraverse.o

# 安装
cp -v *.h $(realpath ../llvm-root/usr/local/include)
cp -v *.a $(realpath ../llvm-root/usr/local/lib)
cp -v *.so* $(realpath ../llvm-root/usr/local/lib)

编译

# 配置项目
cmake -G Ninja -S runtimes -B build \
-DLLVM_ENABLE_RUNTIMES="compiler-rt" \
-DCMAKE_C_COMPILER=zig-cc \
-DCMAKE_CXX_COMPILER=zig-c++ \
-DCMAKE_CXX_FLAGS=""  \
-DCMAKE_BUILD_TYPE=Release \
-DCOMPILER_RT_USE_LIBCXX=ON \
-DLLVM_TARGETS_TO_BUILD="X86;RISCV" \
-DLLVM_DEFAULT_TARGET_TRIPLE=x86_64-unknown-linux-musl \
-DCOMPILER_RT_USE_BUILTINS_LIBRARY=ON \
-DCMAKE_C_COMPILER_TARGET="x86_64-unknown-linux-musl" \
-DCOMPILER_RT_DEFAULT_TARGET_ONLY=ON



-DCOMPILER_RT_DEFAULT_TARGET_TRIPLE=x86_64-unknown-linux-musl


-DCMAKE_C_FLAGS="--sysroot=/lib/musl -L $(realpath ../llvm-root/usr/local/lib) -isystem $(realpath ../llvm-root/usr/local/include) -rtlib=compiler-rt -stdlib=libc++ -lexecinfo -Wl,--as-needed" \
-DCOMPILER_RT_USE_BUILTINS_LIBRARY=ON \
-DCMAKE_C_COMPILER_TARGET="x86_64-unknown-linux-musl" \
-DCOMPILER_RT_DEFAULT_TARGET_ONLY=ON

 \-DCOMPILER_RT_USE_LLVM_UNWINDER=ON \
-DLLVM_USE_LINKER=lld \
-DLIBCXXABI_USE_LLVM_UNWINDER=ON \
-DLIBCXX_HAS_MUSL_LIBC=yes \
-DLLVM_ENABLE_LIBCXX=ON \
-DLLVM_STATIC_LINK_CXX_STDLIB=ON \
-DCOMPILER_RT_USE_LLVM_UNWINDER=ON \

参考

  1. crt1.o, crti.o, crtbegin.o, crtend.o, crtn.o: https://blog.csdn.net/farmwang/article/details/73195951
  2. 关于misc libc / gcc crt文件的迷你常见问题解答: https://codingdict.com/questions/44756
  3. PIE详解: https://nuoye-blog.github.io/2020/05/22/ddb258d5/
  4. c++ - -static-libstdc++可以在g++上使用,但不能在纯gcc上使用?: https://www.coder.work/article/800637
  5. 使用 musl 和 clang 编译 C 程序: https://www.oschina.net/question/12_241258
  6. 构建 libc++ : https://libcxx.llvm.org/BuildingLibcxx.html
  7. 组装完整的工具链: https://clang.llvm.org/docs/Toolchain.html
  8. Clang/Bootstrapping: https://wiki.gentoo.org/wiki/Clang/Bootstrapping
  9. 使用 gnu 编译器 (gcc) 创建共享和静态库: https://renenyffenegger.ch/notes/development/languages/C-C-plus-plus/GCC/create-libraries/index
1
https://gitee.com/coder_lw/wiki.git
git@gitee.com:coder_lw/wiki.git
coder_lw
wiki
wiki
master

搜索帮助