0%

Cpp-Trivial

阅读更多

1 编译过程

graph TD
other_target(["其他目标代码"])
linker[["链接器"]]
source(["源代码"])
compiler[["编译器"]]
assembly(["汇编代码"])
assembler[["汇编器"]]
target(["目标代码"])
lib(["库文件"])
result_target(["目标代码"])
exec(["可执行程序"])
result_lib(["库文件"])

other_target --> linker

subgraph 编译过程
source --> compiler
compiler --> assembly
assembly --> assembler
assembler --> target
end
target --> linker
lib --> linker

linker --> result_target
linker --> exec
linker --> result_lib

2 lib

2.1 静态链接库

后缀:*.a

2.2 动态链接库

后缀:*.so

2.2.1 动态库搜索顺序

搜索如下:详见man ld.so

  1. 在环境变量LD_LIBRARY_PATH指定的目录下搜索
  2. /etc/ld.so.cache指定的目录中搜索
  3. /lib/lib64中搜索(系统发行版安装的)
  4. /usr/lib/usr/lib64中搜索(其他软件安装的)

2.2.2 Linux下so的版本机制介绍

本小节转载摘录自一文读懂Linux下动态链接库版本管理及查找加载方式

/lib64/usr/lib64/usr/local/lib64目录下,会看到很多具有下列特征的软连接,其中xyz为数字, 那么这些软连接和他们后面的数字有什么用途呢?

1
2
3
libfoo.so    ->  libfoo.so.x
libfoo.so.x -> libfoo.so.x.y.z
libbar.so.x -> libbar.so.x.y

这里的xyz分别代表的是这个so的主版本号(MAJOR),次版本号(MINOR),以及发行版本号(RELEASE),对于这三个数字各自的含义,以及什么时候会进行增长,不同的文献上有不同的解释,不同的组织遵循的规定可能也有细微的差别,但有一个可以肯定的事情是:主版本号(MAJOR)不同的两个so库,所暴露出的API接口是不兼容的。而对于次版本号,和发行版本号,则有着不同定义,其中一种定义是:次要版本号表示API接口的定义发生了改变(比如参数的含义发生了变化),但是保持向前兼容;而发行版本号则是函数内部的一些功能调整、优化、BUG修复,不涉及API接口定义的修改

2.2.2.1 几个so库有关名字的介绍

介绍一下在so查找过程中的几个名字

  • SONAME:一组具有兼容APIso库所共有的名字,其命名特征是lib<库名>.so.<数字>这种形式的
  • real name:是真实具有so库可执行代码的那个文件,之所以叫real是相对于SONAMElinker name而言的,因为另外两种名字一般都是一个软连接,这些软连接最终指向的文件都是具有real name命名形式的文件。real name的命名格式中,可能有2个数字尾缀,也可能有3个数字尾缀,但这不重要。你只要记住,真实的那个,不是以软连接形式存在的,就是一个real name
  • linker name:这个名字只是给编译工具链中的连接器使用的名字,和程序运行并没有什么关系,仅仅在链接得到可执行文件的过程中才会用到。它的命名特征是以lib开头,以.so结尾,不带任何数字后缀的格式

2.2.2.2 SONAME的作用

假设在你的Linux系统中有3个不同版本的bar共享库,他们在磁盘上保存的文件名如下:

  • /usr/lib64/libbar.so.1.3
  • /usr/lib64/libbar.so.1.5
  • /usr/lib64/libbar.so.2.1

假设以上三个文件,都是真实的so库文件,而不是软连接,也就是说,上面列出的名字都是real name

根据我们之前对版本号的定义,我们可以知道:

  • libbar.so.1.3libbar.so.1.5之间是互相兼容的
  • libbar.so.2.1和上述两个库之间互相不兼容

引入软连接的好处是什么呢?假设有一天,libbar.so.2.1库进行了升级,但API接口仍然保持兼容,升级后的库文件为libbar.so.2.2,这时候,我们只要将之前的软连接重新指向升级后的文件,然后重新启动B程序,B程序就可以使用全新版本的so库了,我们并不需要去重新编译链接来更新B程序

总结一下上面的逻辑:

  • 通常SONAME是一个指向real name的软连接
  • 应用程序中存储自己所依赖的so库的SONAME,也就是仅保证主版本号能匹配就行
  • 通过修改软连接的指向,可以让应用程序在互相兼容的so库中方便切换使用哪一个
  • 通常情况下,大家使用最新版本即可,除非是为了在特定版本下做一些调试、开发工作

2.2.2.3 linker name的作用

上一节中我们提到,可执行文件里会存储精确到主版本号的SONAME,但是在编译生成可执行文件的过程中,编译器怎么知道应该用哪个主版本号呢?为了回答这个问题,我们从编译链接的过程来梳理一下

假设我们使用gcc编译生成一个依赖foo库的可执行文件Agcc A.c -lfoo -o A

熟悉gcc编译的读者们肯定知道,上述的-l标记后跟随了foo参数,表示我们告诉gcc在编译的过程中需要用到一个外部的名为foo的库,但这里有一个问题,我们并没有说使用哪一个主版本,我们只给出了一个名字。为了解决这个问题,软链接再次发挥作用,具体流程如下:

根据linux下动态链接库的命名规范,gcc会根据-lfoo这个标识拼接出libfoo.so这个文件名,这个文件名就是linker name,然后去尝试读取这个文件,并将这个库链接到生成的可执行文件A中。在执行编译前,我们可以通过软链接的形式,将libfoo.so指向一个具体so库,也就是指向一个real name,在编译过程中,gcc会从这个真实的库中读取出SONAME并将它写入到生成的可执行文件A中。例如,若libfoo.so指向libfoo.so.1.5,则生成的可执行文件A使用主版本号为1SONAME,即libfoo.so.1

在上述编译过程完成之后,SONAME已经被写入可执行文件A中了,因此可以看到linker name仅仅在编译的过程中,可以起到指定连接那个库版本的作用,除此之外,再无其他作用

总结一下上面的逻辑:

  • 通常linker name是一个指向real name的软连接
  • 通过修改软连接的指向,可以指定编译生成的可执行文件使用那个主版本号so
  • 编译器从软链接指向的文件里找到其SONAME,并将SONAME写入到生成的可执行文件中
  • 通过改变linker name软连接的指向,可以将不同主版本号的SONAME写入到生成的可执行文件中

2.3 LD_PRELOAD

环境变量LD_PRELOAD指定的目录拥有最高优先级

2.3.1 demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
cat > sample.c << 'EOF'
#include <stdio.h>
int main(void) {
printf("Calling the fopen() function...\n");
FILE *fd = fopen("test.txt","r");
if (!fd) {
printf("fopen() returned NULL\n");
return 1;
}
printf("fopen() succeeded\n");
return 0;
}
EOF
gcc -o sample sample.c

./sample
#-------------------------↓↓↓↓↓↓-------------------------
Calling the fopen() function...
fopen() returned NULL
#-------------------------↑↑↑↑↑↑-------------------------

touch test.txt
./sample
#-------------------------↓↓↓↓↓↓-------------------------
Calling the fopen() function...
fopen() succeeded
#-------------------------↑↑↑↑↑↑-------------------------

cat > myfopen.c << 'EOF'
#include <stdio.h>
FILE *fopen(const char *path, const char *mode) {
printf("This is my fopen!\n");
return NULL;
}
EOF

gcc -o myfopen.so myfopen.c -Wall -fPIC -shared

LD_PRELOAD=./myfopen.so ./sample
#-------------------------↓↓↓↓↓↓-------------------------
Calling the fopen() function...
This is my fopen!
fopen() returned NULL
#-------------------------↑↑↑↑↑↑-------------------------

2.4 libc、glic、libm、libz以及其他常用动态库

libc实现了C的标准库函数(例如strcpy()),以及POSIX函数(例如系统调用getpid())。此外,不是所有的C标准库函数都包含在libc中,比如大多数math相关的库函数都封装在libm中,大多数压缩相关的库函数都封装在libz

系统调用有别于普通函数,它无法被链接器解析。实现系统调用必须引入平台相关的汇编指令。我们可以通过手动实现这些汇编指令来完成系统调用,或者直接使用libc(它已经为我们封装好了)

glibc, GNU C Library可以看做是libc的另一种实现,它不仅包含libc的所有功能还包含libm以及其他核心库,比如libpthread

其他常用动态库可以参考Library Interfaces and Headers中的介绍

  1. libdl:dynamic linking library
    • libdl主要作用是将那些早已存在于libc中的private dl functions对外露出,便于用户实现一些特殊的需求。dlopen in libc and libdl
    • 可以通过readelf -s /lib64/ld-linux-x86-64.so.2 | grep PRIVATE查看露出的这些方法

2.5 参考

3 内存管理

涉及的系统调用(只涉及虚拟内存,物理内存只能通过缺页异常来分配)

  • brk:详见man brk
  • sbrk:详见man sbrk
  • mmap/munmap:详见man mmap/munmap

而内存管理的lib工作在用户态,底层还是通过上述系统调用来分配虚拟内存,不同的lib管理内存的算法可能有差异

3.1 tcmalloc

如何安装:

1
yum -y install gperftools gperftools-devel

3.1.1 heapprofile

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdlib.h>

void* create(unsigned int size) {
return malloc(size);
}

void create_destory(unsigned int size) {
void* p = create(size);
free(p);
}

int main(void) {
const int loop = 4;
char* a[loop];
unsigned int mega = 1024 * 1024;

for (int i = 0; i < loop; i++) {
const unsigned int create_size = 1024 * mega;
create(create_size);

const unsigned int malloc_size = 1024 * mega;
a[i] = (char*)malloc(malloc_size);

const unsigned int create_destory_size = mega;
create_destory(create_destory_size);
}

for (int i = 0; i < loop; i++) {
free(a[i]);
}

return 0;
}

编译:

1
gcc -o main main.cpp -Wall -O3 -lstdc++ -ltcmalloc -std=gnu++17

运行:

1
2
3
4
# 开启 heap profile 功能
export HEAPPROFILE=/tmp/test-profile

./main

输出如下:

1
2
3
4
5
6
7
Starting tracking the heap
tcmalloc: large alloc 1073741824 bytes == 0x2c46000 @ 0x7f6a8fd244ef 0x7f6a8fd43e76 0x400571 0x7f6a8f962555 0x4005bf
Dumping heap profile to /tmp/test-profile.0001.heap (1024 MB allocated cumulatively, 1024 MB currently in use)
Dumping heap profile to /tmp/test-profile.0002.heap (2048 MB allocated cumulatively, 2048 MB currently in use)
Dumping heap profile to /tmp/test-profile.0003.heap (3072 MB allocated cumulatively, 3072 MB currently in use)
Dumping heap profile to /tmp/test-profile.0004.heap (4096 MB allocated cumulatively, 4096 MB currently in use)
Dumping heap profile to /tmp/test-profile.0005.heap (Exiting, 0 bytes in use)

使用pprof分析内存:

1
2
3
4
5
# 文本格式
pprof --text ./main /tmp/test-profile.0001.heap | head -30

# 图片格式
pprof --svg ./main /tmp/test-profile.0001.heap > heap.svg

3.2 jemalloc

3.3 mimalloc

3.4 对比

What are the differences between (and reasons to choose) tcmalloc/jemalloc and memory pools?

3.5 参考

4 Address Sanitizer

4.1 memory leak

1
2
3
4
5
6
7
8
9
10
11
12
cat > test_memory_leak.cpp << 'EOF'
#include <iostream>

int main() {
int *p = new int(5);
std::cout << *p << std::endl;
return 0;
}
EOF

gcc test_memory_leak.cpp -o test_memory_leak -g -lstdc++ -fsanitize=address -static-libasan
./test_memory_leak

4.2 stack buffer underflow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
cat > test_stack_buffer_underflow.cpp << 'EOF'
#include <iostream>

int main() {
char buffer[5] = "";
uint8_t num = 5;

buffer[-1] = 7;

std::cout << (void*)buffer << std::endl;
std::cout << (void*)&buffer[-1] << std::endl;
std::cout << (void*)&num << std::endl;
std::cout << (uint32_t)num << std::endl;

return 0;
}
EOF

# 非asan模式
gcc test_stack_buffer_underflow.cpp -o test_stack_buffer_underflow -g -lstdc++
./test_stack_buffer_underflow

# asan模式
gcc test_stack_buffer_underflow.cpp -o test_stack_buffer_underflow -g -lstdc++ -fsanitize=address -static-libasan
./test_stack_buffer_underflow

4.3 参考

5 gun工具链

  1. ld:the GNU linker
  2. as:the GNU assembler
  3. gold:a new, faster, ELF only linker
  4. addr2line:Converts addresses into filenames and line numbers
  5. ar:A utility for creating, modifying and extracting from archives
  6. c++filt - Filter to demangle encoded C++ symbols.
  7. dlltool - Creates files for building and using DLLs.
  8. elfedit - Allows alteration of ELF format files.
  9. gprof - Displays profiling information.
  10. nlmconv - Converts object code into an NLM.
  11. nm - Lists symbols from object files.
  12. objcopy - Copies and translates object files.
  13. objdump - Displays information from object files.
  14. ranlib - Generates an index to the contents of an archive.
  15. readelf - Displays information from any ELF format object file.
  16. size - Lists the section sizes of an object or archive file.
  17. strings - Lists printable strings from files.
  18. strip - Discards symbols.
  19. windmc - A Windows compatible message compiler.
  20. windres - A compiler for Windows resource files.

5.1 gcc

5.1.1 编译选项

  1. -E:生成预处理文件(.i
  2. -S:生成汇编文件(.s
    • -fverbose-asm:带上一些注释信息
  3. -c:生成目标文件(.o
  4. 默认生成可执行文件

5.1.2 编译优化

  1. -O0(默认):不做任何优化
  2. -O/-O1:在不影响编译速度的前提下,尽量采用一些优化算法降低代码大小和可执行代码的运行速度
  3. -O2:该优化选项会牺牲部分编译速度,除了执行-O1所执行的所有优化之外,还会采用几乎所有的目标配置支持的优化算法,用以提高目标代码的运行速度
  4. -O3:该选项除了执行-O2所有的优化选项之外,一般都是采取很多向量化算法,提高代码的并行执行程度,利用现代CPU中的流水线,Cache等
  • 不同优化等级对应开启的优化参数参考man page

5.1.3 其他参数

  1. -fsized-deallocation:启用接收size参数的delete运算符。C++ Sized Deallocation。现代内存分配器在给对象分配内存时,需要指定大小,出于空间利用率的考虑,不会在对象内存周围存储对象的大小信息。因此在释放对象时,需要查找对象占用的内存大小,查找的开销很大,因为通常不在缓存中。因此,编译器允许提供接受一个size参数的global delete operator,并用这个版本来对对象进行析构

5.2 ld

种类

  • GNU gold
  • LLVM lld
  • mold

Flags

  • -fuse-ld=gold
  • -B/usr/local/bin/gcc-mold

5.3 参考

6 llvm工具链

6.1 clang-format

如何安装clang-format

1
npm install -g clang-format

如何使用:在用户目录或者项目根目录中创建.clang-format文件用于指定格式化的方式,下面给一个示例

  • 优先使用项目根目录中的.clang-format;如果不存在,则使用用户目录中的~/.clang-format
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
---
Language: Cpp
BasedOnStyle: Google
AccessModifierOffset: -4
AllowShortFunctionsOnASingleLine: Inline
ColumnLimit: 120
ConstructorInitializerIndentWidth: 8 # double of IndentWidth
ContinuationIndentWidth: 8 # double of IndentWidth
DerivePointerAlignment: false # always use PointerAlignment
IndentCaseLabels: false
IndentWidth: 4
PointerAlignment: Left
ReflowComments: false
SortUsingDeclarations: false
SpacesBeforeTrailingComments: 1

注意:

  • 即便.clang-format相同,不同版本的clang-format格式化的结果也有差异

7 其他

7.1 动态分析

analysis-tools

7.2 头文件搜索路径

头文件#include "xxx.h"的搜索顺序

  1. 先搜索当前目录
  2. 然后搜索-I参数指定的目录
  3. 再搜索gcc的环境变量CPLUS_INCLUDE_PATH(C程序使用的是C_INCLUDE_PATH
  4. 最后搜索gcc的内定目录,包括:
    • /usr/include
    • /usr/local/include
    • /usr/lib/gcc/x86_64-redhat-linux/<gcc version>/include(C头文件)或者/usr/include/c++/<gcc version>(C++头文件)

头文件#include <xxx.h>的搜索顺序

  1. 先搜索-I参数指定的目录
  2. 再搜索gcc的环境变量CPLUS_INCLUDE_PATH(C程序使用的是C_INCLUDE_PATH
  3. 最后搜索gcc的内定目录,包括:
    • /usr/include
    • /usr/local/include
    • /usr/lib/gcc/x86_64-redhat-linux/<gcc version>/include(C头文件)或者/usr/include/c++/<gcc version>(C++头文件)

7.3 doc

  1. cpp reference
  2. cppman
    • 安装:pip install cppman
    • 示例:cppman vector::begin

7.4 参考