C program compile&run process

编译总流程

编译是指编译器读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码。

  1. 预处理阶段:
    1. #include语句以及一些宏插入程序文本中,得到main.isum.i文件。
  2. 编译阶段:
    1. 将文本文件main.isum.i编译成文本文件main.ssum.c的汇编语言程序。
      低级的汇编语言为不同的高级语言提供了通用输出语言。
  3. 汇编阶段:
    1. main.ssum.s翻译成机器语言的二进制指令,并打包成一种叫做可重定位目标程序的格式,并将结果保存在main.o和sum.o两个文件中。这种文件格式就比较接近elf格式了。
  4. 链接阶段:
    1. 合并main.osum.o,得到可执行目标文件,就是elf格式文件。


目标文件

目标文件有三种形式:

  • 可重定位目标文件。包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件。
  • 可执行目标文件。包含二进制代码和数据,其形式可以被直接复制到内存并执行。
  • 共享目标文件。一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态地加载进内存并链接。

1 预处理

  • 预处理器: 将.c 文件转化成 .i文件.

生成预处理文件

  • 使用的gcc命令是:gcc –E filename.cpp -o filename.i
    • -E Preprocess only; do not compile, assemble or link.
    • 通过-C能保留头文件里的注释,如gcc -E -C circle.c -o circle.c
  • 另一种方式 gcc -save-temps -c -o main.o main.c
  • 也可以调用cpp filename.cpp -o filename.i命令

理解预处理文件

  • 输出文件会出现许多名叫 linemarkers类似# linenum filename flags的注释,这些注释是为了让编译器能够定位到源文件的行号,以便于编译器能够在编译错误时给出正确的行号。
    • They mean that the following line originated in file filename at line linenum.
    • flags meaning
      • ‘1’ This indicates the start of a new file.
      • ‘2’ This indicates returning to a file (after having included another file)
      • ‘3’ This indicates that the following text comes from a system header file, so certain warnings should be suppressed
      • ‘4’ This indicates that the following text should be treated as being wrapped in an implicit extern “C” block.
        • ‘4’表示接下来的文本应被视为被包含在隐式的“extern “C””块中。在C++中,函数名和变量名可以有不同的命名空间,但是使用“extern “C””修饰时可以取消这种区别,使得函数名和变量名可以在C++和C代码之间共享。因此,在C++中使用“extern “C””来声明C函数或变量时,需要使用‘4’来指示编译器此处的文本应该被视为C代码,而不是C++代码。[来自chatGPT的解释]

预处理内容(过程)

除开注释被替换成空格,包括代码里的预处理命令:

  1. #error "text" 的作用是在编译时生成一个错误消息,它会导致编译过程中断。 同理有#warning
  2. 宏定义指令,如 #define a b 对于这种伪指令,预编译所要做的是将程序中的所有a用b替换,但作为字符串常量的 a则不被替换。还有 #undef,则将取消对某个宏的定义,使以后该串的出现不再被替换。
  3. 条件编译指令,如#ifdef SNIPER#if defined SNIPER && SNIPER == 0,#ifndef,#else,#elif,#endif等。 这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉
    1. gcc编译使用-DSNIPER=5
  4. 头文件包含指令,如#include "FileName"或者#include 等。
    该指令将头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。
  5. 特殊符号,预编译程序可以识别一些特殊的符号。
    例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。
    预编译程序对于在源程序中出现的这些串将用合适的值进行替换。

头文件搜索优先级

#include "" vs #include <> 区别在于前者会在文件的当前目录寻找,但是后者只会在编译器编译的official路径寻找

通常的搜索顺序是:

  • 包含指定源文件的目录(对于在 #include 命令中以引号包括的文件名)。

  • 采用-iquote选项指定的目录,依照出现在命令行中的顺序进行搜索。只对 #include 命令中采用引号的头文件名进行搜索。

  • 所有header file的搜寻会从-I开始, 依照出现在命令行中的顺序进行搜索。(可以使用-I/path/file只添加一个头文件,尤其是在编译的兼容性修改时)

  • 采用环境变量 CPATH 指定的目录。

  • 采用-isystem选项指定的目录,依照出现在命令行中的顺序进行搜索。

  • 然后找环境变量 C_INCLUDE_PATH,CPLUS_INCLUDE_PATH,OBJC_INCLUDE_PATH指定的路径

  • 再找系统默认目录(/usr/include、/usr/local/include、/usr/lib/gcc-lib/i386-linux/2.95.2/include......)

  • 通过如下命令可以查看头文件搜索目录 gcc -xc -E -v - < /dev/null 或者 g++ -xc++ -E -v - < /dev/null*. 如果想改,需要重新编译gcc

  • 或者在编译出错时,g++ -H -v查看是不是项目下的同名头文件优先级高于sys-head-file

2 编译优化Compile

  • 将经过预处理之后的程序转换成特定汇编代码(assembly code)的过程
  • 一般将.c/.h或者.i文件转换成.s文件,

生成汇编代码

  • 使用的gcc命令是:gcc –S filename.cpp -o filename.s,对应于
    • -S Compile only; do not assemble or link.
    • 理论上gcc –S filename.i -o filename.s 也是可行的。但是我遇到头文件冲突的问题error: declaration for parameter ‘__u_char’ but no such parameter
  • 编译命令 cc –S filename.cpp -o filename.s
  • 或者cc1命令

编译内容(过程)

  • 词法分析、语法分析、语意分析、中间代码生成,在语法检查、类型检查之后,将其翻译成等价的中间代码表示或汇编代码
  • 优化(-O3
    • 常规优化:删除死代码、减少寄存器传输、常量折叠、提取中间量
    • 高阶优化:循环展开、指针优化、函数内联,自动SIMD向量化
  • 关于内联函数
    • 内联函数是在函数定义前加上关键字inline的函数。它用于请求编译器将函数的代码插入到每个调用该函数的地方,而不是通过函数调用来执行。这样可以减少函数调用的开销,提高程序的执行效率。
    • 内联函数一般适用于函数体较小、频繁调用的函数,但最终是编译器决定是否将函数内联,编译器可以忽略对内联函数的请求。

如果想把 C 语言变量的名称作为汇编语言语句中的注释,可以加上 -fverbose-asm 选项:

1
2
gcc -S -O3 -fverbose-asm ../src/pivot.c -o pivot_O1.s
objdump -Sd ../build/bin/pivot > pivot1.s

理解汇编文件

请阅读 GNU assembly file一文

3 汇编assemble

汇编器:将.s 文件转化成 .o文件,

生成可重定位目标程序

  • 使用的gcc 命令是:gcc –c
    • -c Compile and assemble, but do not link.
  • 汇编命令是 as

汇编过程

  • 汇编实际上指汇编器(as)把汇编语言代码翻译成**目标机器指令(二进制)**的过程。
  • 目标文件中所存放的也就是与源程序等效的目标的机器语言代码。
  • 目标文件由段组成。通常一个目标文件中至少有两个段:
  1. 代码段:该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。
  2. 数据段:主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。

查看理解

  • 查看汇编代码 objdump -Sd ../build/bin/pivot > pivot1.s
    • -S 以汇编代码的形式显示C++原程序代码,如果有debug信息,会显示源代码。
  • nm file.o 查看目标文件中的符号表

注意,这时候的目标文件里的使用的函数可能没定义,需要链接其他目标文件.a .so .o .dll(Dynamic Link Library的缩写,Windows动态链接库)

nm 命令

List symbol names in object files.

no symbols

  • 编译时可能会为了空间优化掉许多符号表,例如 -O3 Release模式
  • 切换成debug模式

常用选项 -CD

  • -C 选项告诉 nm 将 C++ 符号的 mangled 名称转换为原始的、易于理解的名称。常用于.a的静态库。
  • -g:仅显示外部符号。
  • -D / –dynamic:显示动态符号,这在查看共享库(如 .so 文件)时非常有用。
  • -l:显示本地(static)符号。
  • –defined-only:仅显示已定义的符号。

输出

  1. 符号值。默认显示十六进制,也可以指定;
  2. 符号类型。小写表示是本地符号,大写表示全局符号(external);
  3. 符号名称, 如下
符号类型 描述
A 符号值是绝对的。在进一步的连接中,不会被改变。
B 符号位于未初始化数据段(known as BSS).
C 共用(common)符号. 共用符号是未初始化的数据。在连接时,多个共用符号可能采用一个同样的名字,如果这个符号在某个地方被定义,共用符号被认为是未定义的引用.
D 已初始化数据段的符号
G 已初始化数据段中的小目标(small objective)符号. 一些目标文件格式允许更有效的访问小目标数据,比如一个全局的int变量相对于一个大的全局数组。
I 其他符号的直接应用,这是GNU扩展的,很少用了. N 调试符号.
R 只读数据段符号. S 未初始化数据段中的小目标(small object)符号.
T 代码段的符号.
U 未定义符号.
V 弱对象(weak object)符号. 当一个已定义的弱符号被连接到一个普通定义符号,普通定义符号可以正常使用,当一个未定义的弱对象被连接到一个未定义的符号,弱符号的值为0.
W 一个没有被指定一个弱对象符号的弱符号(weak symbol)。 - a.out目标文件中的刺符号(stabs symbol). 这种情况下,打印的下一个值是其他字段,描述字段,和类型。刺符号用于保留调试信息.
? 未知符号类型,或者目标文件特有的符号类型.

查找动态库

顺序

这个顺序是针对G++编译的,但是对于python查找库,有所不同,会从ldconfig设置开始

  1. LD_LIBRARY_PATH 环境变量:用户可以通过设置 LD_LIBRARY_PATH 环境变量来指定额外的库搜索路径。
  2. /etc/ld.so.cache:这是 ldconfig 生成的缓存文件,包含了系统共享库的路径。
  3. 默认路径:如果以上路径都没有找到所需的库,动态链接器会搜索以下默认路径:/lib/usr/lib/lib64(在64位系统上),/usr/lib64(在64位系统上)

LD_LIBRARY_PATH

遍历 LD_LIBRARY_PATH 中的每个目录,并查找包括软链接在内的所有 .so 文件。

title
1
2
3
4
IFS=':' dirs="$LD_LIBRARY_PATH"
for dir in $dirs; do
find -L "$dir" -name "*.so" 2>/dev/null
done

ldconfig

ldconfig 命令用于配置动态链接器的运行时绑定。你可以使用它来查询系统上已知的库文件的位置()。

ldconfig 会扫描

  • 指定的目录(通常是 /lib/usr/lib,以及 /etc/ld.so.conf 中列出的目录),查找共享库文件(.so 文件),
  • 并生成一个缓存文件 /etc/ld.so.cache。这个缓存文件会被动态链接器(ld.so 或 ld-linux.so)使用,以加快共享库的查找速度。
1
2
3
4
5
6
7
8
9
# 查看所有是path 的库
ldconfig -v

# 永久添加一个新的库路
echo "/path/to/your/library" | sudo tee /etc/ld.so.conf.d/your-library.conf
sudo ldconfig

# 查询 libdw.so 的位置:
ldconfig -p | grep libdw

ldd 检查是否链接成功

  • ldd会显示动态库的链接关系,中间的nmU没关系,只需要最终.so对应符号是T即可。
  • 出于安全考虑,建议在使用 ldd 时避免对不可信的可执行文件运行,因为它可能会执行恶意代码。
  • 相对安全的替代方法是使用 readelf -dobjdump -p 来查看库依赖。

ldd原理

  1. 解析 ELF 文件
  • ldd 会首先读取输入的可执行文件或共享库(通常是 ELF 格式的文件)。
  • ELF(Executable and Linkable Format)是一种文件格式,用于存储可执行文件、目标代码、共享库等。
  1. 查找依赖项
  • ELF 文件包含一个段(section),其中列出了所需的共享库的名称和路径。这些信息存储在 ELF 的动态段(.dynamic)中。
  • ldd 通过解析这些信息,识别出需要加载的共享库。
  1. 使用动态链接器
  • ldd 通过调用动态链接器(如 ld-linux.so)来解析和加载这些共享库。
  • 动态链接器负责在运行时加载库并解决符号(symbol),即将函数或变量名称映射到实际内存地址。
  1. 输出结果
  • ldd 列出每个依赖库的名称、路径以及它们在内存中的地址。
  • 如果某个库未找到,ldd 会显示“not found”的提示。

ldd 显示not found的库,不一定程序在执行就找不到

比如conda的库,ldd就无法解析。猜测和python的运行逻辑有关,比如import的使用,自动搜索相关的lib目录。

4 链接过程

通过使用ld命令,将编译好的目标文件连接成一个可执行文件或动态库。

  • 链接器的核心工作就是符号表解析、重定位和库文件链接三个部分。(具体细节看CSAPP7.5-7.7)
    • 符号解析
      • 每个可重定位目标程序中都存在符号表的数据结构,包含了一些被声明的函数和变量的符号。依上例,main.o和sum.o都有一个这样的结构。符号表中的每一项都包含一个符号名字和一个符号定义的地址。
      • 符号解析的任务就是将这些符号和它们所在的源文件、库文件中的定义进行匹配。这个过程会生成符号表,用于给链接器在后续的重定位中找到函数所在的地址。
      • 对于符号解析有重载(不同的类,函数名相同)的特殊情况,比如Foo::bar(int,long)会变成bar__3Fooil。其中3是名字字符数
    • 重定位:在符号解析完成后,链接器会把不同的目标文件合并在一起,此时就需要对目标代码进行地址的修正,使得各个目标文件之间的函数调用或者变量访问都可以正确。这个过程叫做重定位。链接器会根据符号表信息,将每个函数调用位置中的符号替换成实际的地址。
    • 库文件链接:链接器还需要为程序链接不同的库文件,包括系统库和用户库。这些库文件可能是静态库或者动态库。
      • 如果是静态库,链接器会从库文件中提取目标代码并将其与目标文件合并成一个可执行文件。
      • 如果是动态库,则需要在运行时动态加载库文件,并将其链接到应用程序中。

符号和符号表

见 Linux Executable file: Structure & Running

符号解析

  • 局部变量
    • 编译器只允许每个模块中每个局部符号有一个定义。同时确保它们拥有唯一的名字。
  • 全局变量
    • 缺失情况:当编译器遇到一个不是在当前模块中定义的符号(变量或函数名)时,会假设该符号是在其他某个模块中定义的,生成一个链接器符号表条目,并把它交给链接器处理。如果链接器在它的任何输入模块中都找不到这个被引用符号的定义,就输出undefined reference to
    • 同名情况:编译器报错或者选择一个,
      • 函数和已初始化的全局变量是强符号,
      • 未初始化的全局变量是弱符号。
    • 选择规则:
      • 规则 1:不允许有多个同名的强符号。
      • 规则 2:如果有一个强符号和多个弱符号同名,那么选择强符号。
      • 规则 3:如果有多个弱符号同名,那么从这些弱符号中任意选择一个。
      • 规则 2 和规则 3 的应用会造成一些不易察觉的运行时错误,对于不警觉的程序员来说,是很难理解的,尤其是如果重复的符号定义还有不同的类型时。

重定位

一旦链接器完成了符号解析这一步,就把代码中的每个符号引用和正好一个符号定义(即它的一个输入目标模块中的一个符号表条目)关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。现在就可以开始重定位步骤了,在这个步骤中,将合并输入模块,并为每个符号分配运行时地址。重定位由两步组成:

  • 重定位节和符号定义
    • 在这一步中,链接器将所有相同类型的节合并为同一类型的新的聚合节。
      • 例如,来自所有输入模块的.data 节被全部合并成一个节,这个节成为输出的可执行目标文件的.data 节。
      • 然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。
      • 当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。
  • 重定位节中的符号引用
    • 在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。
    • 要执行这一步,链接器依赖于可重定位目标模块中称为重定位条目(relocation entry)的数据结构,我们接下来将会描述这种数据结构。

重定位条目

当汇编器生成一个目标模块时,它并不知道数据和代码最终将放在内存中的什么位置。它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。所以,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。

代码的重定位条目放在 .rel.text 中。已初始化数据的重定位条目放在 .rel.data 中。

下面 展示了 ELF 重定位条目的格式。

  1. offset 是需要被修改的引用的节偏移。
  2. symbol 标识被修改引用应该指向的符号。
  3. type 告知链接器如何修改新的引用。
    1. ELF 定义了 32 种不同的重定位类型,有些相当隐秘。我们只关心其中两种最基本的重定位类型:
      1. R_X86_64_PC32。重定位一个使用 32 位 PC 相对地址的引用。回想一下 3.6.3 节,一个 PC 相对地址就是距程序计数器(PC)的当前运行时值的偏移量。当 CPU 执行一条使用 PC 相对寻址的指令时,它就将在指令中编码的 32 位值加上 PC 的当前运行时值,得到有效地址(如 call 指令的目标),PC 值通常是下一条指令在内存中的地址。(将 PC 压入栈中来使用)
      2. R_X86_64_32。重定位一个使用 32 位绝对地址的引用。通过绝对寻址,CPU 直接使用在指令中编码的 32 位值作为有效地址,不需要进一步修改。
  4. addend 是一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整。
1
2
3
4
5
6
typedef struct {
long offset; /* Offset of the reference to relocate */
long type:32, /* Relocation type */
symbol:32; /* Symbol table index */
long addend; /* Constant part of relocation expression */
} Elf64_Rela;

目标文件与库的位置

链接器通常从左到右解析依赖项,这意味着如果库 A 依赖于库 B,那么库 B 应该在库 A 之前被链接。

库顺序

假设有三个库 libA, libB, 和 libC,其中 libA 依赖 libB,而 libB 又依赖 libC。在 CMake 中,你应该这样链接它们:

1
target_link_libraries(your_target libC libB libA)

这样的顺序确保了当链接器处理 libA 时,libB 和 libC 中的符号已经可用。

书上截图



4.1 静态链接

静态库static library就是将相关的目标模块打包形成的单独的文件。使用ar命令。

  • 在Linux系统中,静态库以一种称为存档(archive)的特殊文件格式存放在磁盘中。
  • 存档文件是一组连接起来的可重定位目标文件的集合,有一个头部用来描述每个成员目标文件的大小和位置。
  • 存档文件名由后缀.a标识。

优点与问题

静态库的优点在于:

  • 程序员不需要显式的指定所有需要链接的目标模块,因为指定是一个耗时且容易出错的过程;
  • 链接时,连接程序只从静态库中拷贝被程序引用的目标模块,这样就减小了可执行文件在磁盘和内存中的大小。

问题:

  • 几乎所有程序都需要printf这样的库函数,每个可执行文件都包含该模块的代码段和数据段,浪费磁盘空间。
  • linux采用虚拟内存管理内存分配,每个进程的内存空间是独立的,运行时所有程序都要把这些库函数代码段和数据段加载到自己的内存里,浪费内存。
  • 静态库和所有的软件一样,需要定期维护和更新。如果应用程序员想要使用一个库的最新版本,他们必须以某种方式了解到该库的更新情况,然后显式地将他们的程序与更新了的库重新链接。

静态链接过程

深入理解计算机系统P477,静态库例子

1
gcc -static -o prog2c main2.o -L. -lvector

图 7-8 概括了链接器的行为。-static 参数告诉编译器驱动程序,链接器应该构建一个完全链接的可执行目标文件,它可以加载到内存并运行,在加载时无须更进一步的链接。-lvector 参数是 libvector.a 的缩写,-L. 参数告诉链接器在当前目录下查找 libvector.a。

  • 当链接器运行时,它判定 main2.o 引用了 addvec.o 定义的 addvec 符号,所以复制 addvec.o 到可执行文件。
  • 因为程序不引用任何由 multvec.o 定义的符号,所以链接器就不会复制这个模块到可执行文件。
  • 链接器还会复制 libc.a 中的 printf.o 模块,以及许多 C 运行时系统中的其他模块。

4.2 动态链接

  • 共享库(shared library)是致力于解决静态库缺陷的一个现代创新产物。共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。
    • 共享库也称为共享目标(shared object),在 Linux 系统中通常用 .so 后缀来表示。微软的操作系统大量地使用了共享库,它们称为 DLL(动态链接库)。
  • 这个过程称为动态链接(dynamic linking),是由一个叫做动态链接器(dynamic linker)的程序来执行的。

共享库是以两种不同的方式来“共享”的:

  • 首先,在任何给定的文件系统中,对于一个库只有一个. so 文件。所有引用该库的可执行目标文件共享这个 .so 文件中的代码和数据,而不是像静态库的内容那样被复制和嵌入到引用它们的可执行的文件中。
  • 其次,在内存中,一个共享库的 .text 节的一个副本可以被不同的正在运行的进程共享

ld-link

如上创建了一个可执行目标文件 prog2l,而此文件的形式使得它在运行时可以和 libvector.so 链接。基本的思路是:

  • 当创建可执行文件时,静态执行一些链接
    • 此时,没有任何 libvector.so 的代码和数据节真的被复制到可执行文件 prog2l 中。反之,链接器复制了一些重定位和符号表信息,它们使得运行时可以解析对 libvector.so 中代码和数据的引用。
  • 然后在程序加载时,动态完成链接过程。
    • 动态链接可以在可执行文件第一次加载和运行时发生(加载时链接)
      • Common case for Linux,handled automatically by the dynamic linker (ld-linux.so).
      • Standard C library (libc.so)usually dynamically linked.
    • 动态链接也可以在程序开始运行后发生(运行时链接).
      • In Linux,this is done by calls to the dlopen() interface.
        • Distributing software.
        • High-performance web servers.
        • Runtime library interpositioning.

加载情况一

情况:在应用程序被加载后执行前时,动态链接器加载和链接共享库的情景。

核心思想:由动态链接器接管,加载管理和关闭共享库(比如,如果没有其他共享库还在使用这个共享库,dlclose函数就卸载该共享库。)。

  1. 首先,加载部分链接的可执行文件 prog2l。
  2. prog2l 包含一个 .interp 节,这一节包含动态链接器的路径名,动态链接器本身就是一个共享目标(如在 Linux 系统上的 ld-linux.so). 加载器不会像它通常所做地那样将控制传递给应用,而是加载和运行这个动态链接器。然后,动态链接器通过执行下面的重定位完成链接任务:
    1. 重定位 libc.so 的文本和数据到某个内存段。
    2. 重定位 libvector.so 的文本和数据到另一个内存段。
    3. 重定位 prog2l 中所有对由 libc.so 和 libvector.so 定义的符号的引用。

最后,动态链接器将控制传递给应用程序。从这个时刻开始,共享库的位置就固定了,并且在程序执行的过程中都不会改变。

加载情况二

情况:应用程序在运行时要求动态链接器加载和链接某个共享库,而无需在编译时将那些库链接到应用。

实际应用情况:

  • 分发软件。微软 Wmdows 应用的开发者常常利用共享库来分发软件更新。他们生成一个共库的新版本,然后用户可以下载,并用它替代当前的版本。下一次他们运行应用程序时,应用将自动链接和加载新的共享库。
  • 构建高性能 Web 服务器。
    • 许多 Web 服务器生成动态内容,比如个性化的 Web 页面、账户余额和广告标语
    • 早期的 Web 服务器通过使用 fork 和 execve 创建一个子进程,并在该子进程的上下文中运行 CGI 程序来生成动态内容。
    • 然而,现代高性能的 Web 服务器可以使用基于动态链接的更有效和完善的方法来生成动态内容。

思路是将每个生成动态内容的函数打包在共享库中。

  1. 当一个来自 Web 浏览器的请求到达时,服务器动态地加载和链接适当的函数,然后直接调用它,而不是使用 fork 和 execve 在子进程的上下文中运行函数。
  2. 函数会一直缓存在服务器的地址空间中,所以只要一个简单的函数调用的开销就可以处理随后的请求了。这对一个繁忙的网站来说是有很大影响的。更进一步地说,在运行时无需停止服务器,就可以更新已存在的函数,以及添加新的函数。

动态库的优点

  • 更新动态库,无需重新链接;对于大系统,重新链接是一个非常耗时的过程;
  • 运行中可供多个程序使用,内存中只需要有一份,节省内存。运行时一个共享库的代码段和数据段在物理内存中只有一份,但映射到多个虚拟内存片段上,供不同程序使用。其中代码段是只读的,整个操作系统绝对只有一份。但数据段有可能被修改,在修改的时候则会复制一个副本,每个进程有自己的一个内存副本。
  • 共享库是.so文件,不会和我们自己的代码一起合并成可执行文件,不占磁盘空间。

动态链接

fPIC,fPIE

编译器yasm的参数-DPIE

如果同一份代码可能被加载到进程空间的任意虚拟地址上执行(如共享库和动态加载代码),那么就需要使用-fPIC生成位置无关代码。

如何实现动态链接

  1. 共享库是.so文件,不会和我们自己的代码一起合并成可执行文件,不占磁盘空间。
  2. 运行时一个共享库的代码段和数据段在物理内存中只有一份,但映射到多个虚拟内存片段上,供不同程序使用。
    1. 其中代码段是只读的,整个操作系统绝对只有一份。
    2. 但数据段有可能被修改,在修改的时候则会复制一个副本,每个进程有自己的一个内存副本。
  3. 共享库的代码段和数据段加载到任意的内存段中,位置不固定。
  4. 加载完成后,进行符号重定位。回想一下之前说过的重定位过程,需要修改所有符号引用的地址。
    1. 由于动态链接在运行时才确定共享库代码段和数据段的内存地址,所以在运行时才能进行重定位。
    2. 运行时修改代码,想想就觉得不优雅。而且Linux不允许在运行时修改代码段。
  5. 由此,要完成动态链接,还需要引入了最后一个重要的概念,位置无关代码,即在加载时无需重定位的代码。

位置无关代码(Position-Independent Code, PIC)

  • 问题:多个进程是如何共享程序的一个副本的呢?

    • 一种方法是给每个共享库分配一个事先预备的专用的(虚拟)地址空间片,然后要求加载器总是在这个地址加载共享库。
  • 问题。

    • 地址空间的使用效率不高,因为即使一个进程不使用这个库,那部分空间还是会被分配出来。
    • 难以管理。我们必须保证没有片会重叠。
      • 库修改了之后,我们必须确认已分配给它的片还适合它的大小。如果不适合了,必须找一个新的片。
      • 创建了一个新的库,我们还必须为它寻找空间。随着时间的进展,假设在一个系统中有了成百个库和库的各个版本库,就很难避免地址空间分裂成大量小的、未使用而又不再能使用的小洞。
      • 更糟的是,对每个系统而言,库在内存中的分配都是不同的,这就引起了更多令人头痛的管理问题。
  • 可以加载而无需重定位的代码称为位置无关代码(Position-Independent Code,PIC)

    • 无限多个进程可以共享一个共享模块的代码段的单一副本。(当然,每个进程仍然会有它自己的读/写数据块。)
  • 在一个 x86-64 系统中,对同一个目标模块中符号的引用是不需要特殊处理使之成为 PIC。可以用 PC 相对寻址来编译这些引用,构造目标文件时由静态链接器重定位。

  • 然而,对共享模块定义的外部过程和对全局变量的引用需要一些特殊的技巧,接下来我们会谈到。

PIC 数据引用

  • 目标:生成对全局变量的 PIC 引用
  • 思想:无论我们在内存中的何处加载一个目标模块(包括共享目标模块),数据段与代码段的距离总是保持不变。因此,代码段中任何指令和数据段中任何变量之间的距离都是一个运行时常量,与代码段和数据段的绝对内存位置是无关的。由于数据段是可以在运行时修改的,所以可以把对代码段的修改转化为对数据段的修改。
  • 实现:在数据段前面加入一个数据结构,全局偏移量表(Global Offset Table,GOT)。每一个被该模块引用的全局数据目标(过程或全局变量),都在GOT里有一个8字节条目,并为每个条目生成一个重定位条目。
  • 实际使用:在加载时,动态链接器会重定位 GOT 中的每个条目,使得它包含目标的正确的绝对地址。然后程序执行时就能正确访问正确的绝对地址了。

PIC 函数调用

  • 情况:假设程序调用一个由共享库定义的函数。编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。
  • 简单方法:为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。不过,这种方法并不是 PIC,因为它需要链接器修改调用模块的代码段。

解决方法:延迟绑定(lazy binding),将过程地址的绑定推迟到第一次调用该过程时。

动机:使用延迟绑定的动机是对于一个像 libc.so 这样的共享库输出的成百上千个函数中,一个典型的应用程序只会使用其中很少的一部分。把函数地址的解析推迟到它实际被调用的地方,能避免动态链接器在加载时进行成百上千个其实并不需要的重定位。

结果:第一次调用过程的运行时开销很大,但是其后的每次调用都只会花费一条指令和一个间接的内存引用。

实现:延迟绑定是通过两个数据结构之间简洁但又有些复杂的交互来实现的,这两个数据结构是:GOT 和过程链接表(Procedure Linkage Table,PLT)。如果一个目标模块调用定义在共享库中的任何函数,那么它就有自己的 GOT 和 PLT。GOT 是数据段的一部分,而 PLT 是代码段的一部分。

首先,让我们介绍这两个表的内容。

  • 过程链接表(PLT)。PLT 是一个数组,其中每个条目是 16 字节代码。
    • PLT[0] 是一个特殊条目,它跳转到动态链接器中。
    • 每个被可执行程序调用的库函数都有它自己的 PLT 条目。每个条目都负责调用一个具体的函数。
    • PLT[1](图中未显示)调用系统启动函数(__libc_start_main),它初始化执行环境,调用 main 函数并处理其返回值从 PLT[2] 开始的条目调用用户代码调用的函数。在我们的例子中,PLT[2] 调用 addvec,PLT[3](图中未显示)调用 printf。
  • 全局偏移量表(GOT)。正如我们看到的,GOT 是一个数组,其中每个条目是 8 字节地址。
    • 和 PLT 联合使用时,GOT[O] 和 GOT[1] 包含动态链接器在解析函数地址时会使用的信息。GOT[2] 是动态链接器在 ld-linux.so 模块中的入口点。
    • 其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的 PLT 条目。例如,GOT[4] 和 PLT[2] 对应于 addvec。初始时,每个 GOT 条目都指向对应 PLT 条目的第二条指令。

PLT

上图a 展示了 GOT 和 PLT 如何协同工作,在 addvec 被第一次调用时,延迟解析它的运行时地址:

  1. 第 1 步。不直接调用 addvec,程序调用进入 PLT[2],这是 addvec 的 PLT 条目。
  2. 第 2 步。第一条 PLT 指令通过 GOT[4] 进行间接跳转。因为每个 GOT 条目初始时都指向它对应的 PLT 条目的第二条指令,这个间接跳转只是简单地把控制传送回 PLT[2] 中的下一条指令。
  3. 第 3 步。在把 addvec 的 ID(0x1)压入栈中之后,PLT[2] 跳转到 PLT[0]。
  4. 第 4 步。PLT[0] 通过 GOT[1] 间接地把动态链接器的一个参数压入栈中,然后通过 GOT[2] 间接跳转进动态链接器中。动态链接器使用两个栈条目来确定 addvec 的运行时位置,用这个地址重写 GOT[4],再把控制传递给 addvec。

上图b 给出的是后续再调用 addvec 时的控制流:

  1. 第 1 步。和前面一样,控制传递到 PLT[2]。
  2. 第 2 步。不过这次通过 GOT[4] 的间接跳转会将控制直接转移到 addvec。

库搜索优先级

静态库

  1. gcc先从-L寻找;
  2. 再找环境变量LIBRARY_PATH指定的搜索路径;
  3. 再找内定目录 /lib /usr/lib /usr/local/lib 这是当初compile gcc时写在程序内的。

动态库

  1. 编译目标代码时指定的动态库搜索路径-L;
  2. 环境变量LD_LIBRARY_PATH指定的动态库搜索路径;
  3. 配置文件/etc/ld.so.conf中指定的动态库搜索路径;
  4. 默认的动态库搜索路径/lib /usr/lib/ /usr/local/lib
1
2
3
4
5
shaojiemike@snode6 /lib/modules/5.4.0-107-generic/build  [06:32:26]
> gcc -print-search-dirs
install: /usr/lib/gcc/x86_64-linux-gnu/9/
programs: =/usr/lib/gcc/x86_64-linux-gnu/9/:/usr/lib/gcc/x86_64-linux-gnu/9/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/9/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/9/../../../../x86_64-linux-gnu/bin/x86_64-linux-gnu/9/:/usr/lib/gcc/x86_64-linux-gnu/9/../../../../x86_64-linux-gnu/bin/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/9/../../../../x86_64-linux-gnu/bin/
libraries: =/usr/lib/gcc/x86_64-linux-gnu/9/:/usr/lib/gcc/x86_64-linux-gnu/9/../../../../x86_64-linux-gnu/lib/x86_64-linux-gnu/9/:/usr/lib/gcc/x86_64-linux-gnu/9/../../../../x86_64-linux-gnu/lib/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/9/../../../../x86_64-linux-gnu/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/9/:/usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/9/../../../../lib/:/lib/x86_64-linux-gnu/9/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/9/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/9/../../../../x86_64-linux-gnu/lib/:/usr/lib/gcc/x86_64-linux-gnu/9/../../../:/lib/:/usr/lib/

5. 加载器

加载器:将可执行程序加载到内存并进行执行,loader和ld-linux.so。

将可执行文件加载运行

其他技巧:GNU用于理解和处理目标文件的相关命令

命令 描述
ar 创建静态库,插入、删除、列出和提取成员;
stringd 列出目标文件中所有可以打印的字符串;
strip 从目标文件中删除符号表信息;
nm 列出目标文件符号表中定义的符号;
size 列出目标文件中节的名字和大小;
readelf 显示一个目标文件的完整结构,包括ELF 头中编码的所有信息。
objdump 显示目标文件的所有信息,最有用的功能是反汇编.text节中的二进制指令。
ldd 列出可执行文件在运行时需要的共享库。

动态查看进程调用命令

ltrace 跟踪进程调用库函数过程
strace 系统调用的追踪或信号产生的情况
Relyze 图形化收费试用

debugging symbols

  • 编译时加入-g选项,可以生成调试信息,这样在gdb中可以查看源代码。
  • 但是在复杂的编译过程中,最后可执行文件丢失了debugging symbols,所以研究一下怎么生成debugging symbols, 编译过程中的传递,以及如何查看。

debugging symbols的内容

1
2
3
4
5
6
7
8
9
10
objdump -g <archive_file>.a
# 如果.o文件有debugging symbols,会输出各section详细信息
Contents of the .debug_aranges section (loaded from predict-c.o):
# 没有则如下
cabac-a.o: file format elf64-x86-64

dct-a.o: file format elf64-x86-64

deblock-a.o: file format elf64-x86-64

生成debugging symbols

  • 预处理过程
    • 应该会保留debugging symbols所需的信息,在实验后发现,执行gcc -E -g testBigExe.cpp -o testDebug.i相对于无-g的命令,只会多一行信息# 1 "/staff/shaojiemike/test/OS//"
  • 编译过程
    • 执行gcc -S -g testBigExe.cpp -o testDebug.s,对比之前的汇编文件,由72行变成9760行。具体解析参考 GNU assembly file一文
    • -g前后
  • 汇编过程:保留了debug信息的汇编代码生成带debug信息的目标文件
  • 链接(Linker)

编译代码中OpenMP实现

简单的#pragma omp for,编译后多出汇编代码如下。当前可以创建多少个线程默认汇编并没有显示的汇编指令。

1
2
3
4
5
6
call omp_get_num_threads@PLT
movl %eax, %ebx
call omp_get_thread_num@PLT
movl %eax, %ecx

call GOMP_barrier@PLT

某些atomic的导语会变成对应汇编

需要进一步的研究学习

  • chatGPT说:后端阶段(例如汇编器和连接器),则主要是对汇编代码和目标代码进行优化,例如指令调度、地址计算、代码缩减等。但是我持严重怀疑态度, 链接过程有这么多优化吗?

遇到的问题

暂无

开题缘由、总结、反思、吐槽~~

基础不牢,地动山摇。ya 了。

参考文献

https://www.cnblogs.com/LiuYanYGZ/p/5574601.html

https://hansimov.gitbook.io/csapp/part2/ch07-linking/7.5-symbols-and-symbol-tables

C++ ABI

摘要

ABI被人熟知,就是编译时,接口不匹配导致运行时的动态库undefined symbol报错。

Read more

Python

解释性语言

解释型语言没有严格编译汇编过程,由解释器将代码块按需要变运行边翻译给机器执行。因此解释型语言一度存在运行效率底,重复解释的问题。但是通过对解释器的优化!可以提高解释型语言的运行效率。

Python 与大多数解释型语言一样,确实是将源代码编译为一组虚拟机指令,并且 Python 解释器是针对相应的虚拟机实现的。这种中间格式被称为 “字节码”。

Python 字节码

Python 以 .pyc 结尾的 “ 字节码(bytecode)” 文件(二进制文件),一般位于__pycache__ 的子目录中,可以避免每次运行 Python 时去重新解析源代码。

1
python -m py_compile file.py  # 生成单个pyc文件

python程序运行中的 Python 字节码 是什么时候产生的,我修改py文件,其对应的字节码文件会在何时更新

gpt3.5: Python字节码是由Python解释器在执行Python源代码之前生成的中间表示形式。它是将源代码翻译为机器可以理解的指令序列的一种形式。字节码文件的扩展名通常为.pyc(Python 3)或.pyo(Python 2)。

当你运行Python程序时,如果存在对应的字节码文件(.pyc或.pyo),解释器会首先检查字节码文件的时间戳。如果字节码文件的时间戳比源代码文件的时间戳更晚,且没有进行代码的修改,解释器会直接加载和执行字节码,跳过源代码的编译过程。

当你修改了Python源代码文件时,字节码文件不会立即更新。Python解释器会在下一次运行程序时,检测到源代码文件的修改,并重新编译生成新的字节码文件。这样,更新后的字节码文件将反映源代码的更改,并用于后续的执行。

访问和理解 Python 字节码

经过编译的python文件可以提高程序的运行速度,一定程度上也对源代码起到了保护作用。然而如果我们只有编译过的python字节码文件,就给我们审查源码造成了一定的困难,这就引出了python字节码反编译的需求。

如果你想玩转字节码,那么,Python 标准库中的 dis 模块将对你有非常大的帮助;dis 模块为 Python 字节码提供了一个 “反汇编”,它可以让你更容易地得到一个人类可读的版本,以及查找各种字节码指令。

知道如何去访问和阅读 Python 字节码将让你很容易回答为什么某些结构比其它结构运行的更快这样的问题(比如,为什么 {} 比 dict() 快)(尝试对比一下: dis.dis(“{}”) 与 dis.dis(“dict()”) 就会明白)。

pyo优化文件

pyo文件是源代码文件经过优化编译后生成的文件,是pyc文件的优化版本。编译时需要使用-O和-OO选项来生成pyo文件。在Python3.5之后,不再使用.pyo文件名,而是生成文件名类似“test.opt-n.pyc的文件。

1
python -O -m py_compile test.py

Python 虚拟机

CPython 使用一个基于栈的虚拟机。(你可以 “推入” 一个东西到栈 “顶”,或者,从栈 “顶” 上 “弹出” 一个东西来)。

CPython 使用三种类型的栈:

  1. 调用栈(call stack)。这是运行 Python 程序的主要结构。它为每个当前活动的函数调用使用了一个东西 —— “ 帧(frame)”
  2. 在每个帧中,有一个 **计算栈(evaluation stack)**(也称为 数据栈(data stack))。这个栈就是 Python 函数运行的地方,运行的 Python 代码大多数是由推入到这个栈中的东西组成的,操作它们,然后在返回后销毁它们。
  3. 在每个帧中,还有一个**块栈(block stack)**。它被 Python 用于去跟踪某些类型的控制结构:循环、try / except 块、以及 with 块,全部推入到块栈中,当你退出这些控制结构时,块栈被销毁。

C vs Python

运行流程区别

python的传统运行执行模式:录入的源代码转换为字节码,之后字节码在python虚拟机中运行。代码自动被编译,之后再解释成机器码在CPU中执行。

c编译器直接把c源代码编译成机器码。过程比python执行过程少了字节码生成和虚拟机执行字节码过程。所以自然比python快。

深、浅拷贝

Python append() 与深拷贝、浅拷贝

python赋值只是引用,别名

1
2
3
4
5
6
7
8
list.append('Google')   ## 使用 append() 添加元素
alist.append( num ) # 浅拷贝 ,之后修改num 会影响alist内的值

import copy
alist.append( copy.deepcopy( num ) ) # 深拷贝

# delete
del list[2]

for循环迭代的元素 也是 引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
original_list = [1, 2, 3]

for item in original_list:
item *= 2 # 每个元素是不可变的

print(original_list)

original_list = [[1,2,3], [2], [3]]

for item in original_list:
item.append("xxx") # 每个元素是可变的

print(original_list)

# [1, 2, 3]
# [[1, 2, 3, 'xxx'], [2, 'xxx'], [3, 'xxx']]

[函数传参是引用,但是能通过切片来得到类似指针](https

参数的传递
函数声明时的形参,使用时,等同于函数体内的局部变量。由于Python中一切皆为对象。因此,参数传递时直接传递对象的地址,但具体使用分两种类型:

  1. 传递不可变对象的引用(起到其他语言值传递的效果) 数字,字符串,元组,function等
  2. 传递可变对象的引用(起到其他语言引用传递的效果) 字典,列表,集合,自定义的对象等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def fun0(a):
a = [0,0] # a在修改后,指向的地址发生改变,相当于新建了一个值为[0,0]

def fun(a):
a[0] = [1,2]

def fun2(a):
a[:] = [10,20]

b = [3,4]
fun0(b)
print(b)
fun(b)
print(b)
fun2(b)
print(b)

# [3, 4]
# [[1, 2], 4]
# [10, 20]

return 返回值, 可变对象的也是引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def fun1(l):
l.append("0")
return l

def fun2(l):
return l

if __name__=="__main__":
l = [1,2,3,4,5]

rel2 = fun2(l)
print(rel2)
rel1 = fun1(l)
print(rel1)
print(rel2)

# [1, 2, 3, 4, 5]
# [1, 2, 3, 4, 5, '0']
# [1, 2, 3, 4, 5, '0']

逻辑

setup

setup安装包的过程,请看pip package一文。

import

命名空间(namespace)可以基本理解成每个文件是一个,通过import来使用

触发 __init__.py

  • 当你导入一个包时,Python 会执行该包目录下的 __init__.py 文件。如果没有这个文件,Python 会认为这个目录不是一个包,因此 import 语句会失败。
  • __init__.py 负责初始化这个包,可以定义一些包级别的变量、函数或导入包的其他子模块。

行为

  • 每次导入包时,__init__.py 文件只会在第一次导入时被执行一次。如果模块已经被导入到当前的命名空间,再次 import 不会重新执行 __init__.py,除非你强制重新加载(比如用 importlib.reload())。
  • import 的执行会触发模块的初始化,类似于 C++ 中构造函数的概念,但不是在对象级别,而是在模块级别。
1
2
3
4
5
# example/__init__.py
print("Initializing the package")

def hello():
print("Hello from the package")
1
2
3
4
import example
# 输出 "Initializing the package"
example.hello()
# 输出 "Hello from the package"

入口

  • 在Python中,if __name__ == "__main__"这种写法通常出现在模块中,它的作用是控制模块的执行流程。
  • 当一个模块被导入时,Python解释器会自动将这个模块的__name__属性设置为模块名称。但是如果模块是被直接运行的,则__name__属性会被设置为字符串__main__。
  • 所以if name == “main”可以用来区分模块是被导入运行还是被直接运行:
  • 如果模块是被导入的,if语句不会执行。因为模块的__name__不等于__main__。
  • 如果模块是被直接运行的,if语句会执行。因为模块的__name__等于__main__。

清理与释放

程序结束时的清理行为(类似析构函数的操作)

在 Python 中,并没有像 C++ 那样显式的析构函数。模块或对象的清理一般通过以下方式实现:

  • 对象的析构:当一个 Python 对象的引用计数降为零时,Python 会自动调用该对象的 __del__ 方法进行资源清理。这个机制类似于 C++ 的析构函数,但触发时机取决于 Python 的垃圾回收机制。
1
2
3
4
5
6
7
8
9
class MyClass:
def __init__(self):
print("Object created")

def __del__(self):
print("Object destroyed")

obj = MyClass()
# 程序结束时,或者当 obj 的引用计数降为 0 时,触发 __del__()
  • 模块的清理:当程序结束时,Python 会尝试清理已加载的模块。这个过程会调用模块内一些特殊的钩子函数来进行必要的清理工作。虽然 Python 没有直接为模块提供析构函数,但是你可以使用 atexit 模块来注册一个函数,确保在程序结束时执行。

示例:使用 atexit 实现模块级别的清理操作

1
2
3
4
5
6
7
8
9
import atexit

def cleanup():
print("Cleaning up resources before program exit")

# 注册一个清理函数,在程序结束时自动调用
atexit.register(cleanup)

print("Program is running")

输出

1
2
Program is running
Cleaning up resources before program exit
  • atexit 模块允许你注册多个函数,它们会在解释器关闭之前按注册顺序依次执行。
  • 这种机制相当于 C++ 中的全局或静态对象析构函数的功能,确保在程序结束时执行一些清理工作。

模块的生命周期总结

  • 初始化:当模块被导入时,Python 会执行模块的顶层代码,包括 __init__.py 文件。这相当于模块的 “构造” 过程。
  • 对象的析构:在 Python 中,通过垃圾回收机制和 __del__ 方法来管理对象的生命周期。通常情况下,当对象不再被引用时,会自动触发清理。
  • 程序结束时的清理:Python 提供了 atexit 模块来执行程序结束时的资源清理操作。你可以在模块中注册一些函数,确保在程序退出时执行清理任务。

与 C++ 的比较

  • Python 的模块和包机制类似于 C++ 中的构造函数,但它的作用范围是模块级别的,而不是对象级别的。
  • Python 通过垃圾回收和 __del__ 方法来处理对象的清理,而不是像 C++ 中的显式析构函数。
  • Python 提供了 atexit 模块来实现程序级别的清理操作,这类似于 C++ 中全局/静态对象的析构行为,但更加灵活。

语法

装饰器 decorator

@能在最小改变函数的情况下,包装新的功能。^1

1
2
3
4
5
6
7
8
9
10
11
12
def use_logging(func):

def wrapper():
logging.warn("%s is running" % func.__name__)
return func()
return wrapper

@use_logging
def foo():
print("i am foo")

foo()

下划线

单下划线、双下划线、头尾双下划线说明:

  • __foo__: 定义的是特殊方法,一般是系统定义名字 ,类似 init() 之类的。
  • _foo: 以单下划线开头的表示的是 protected 类型的变量,即保护类型只能允许其本身与子类进行访问,不能用于 from module import *
  • __foo: 双下划线的表示的是私有类型(private)的变量, 只能是允许这个类本身进行访问了。

函数传参

解包

  • 解包是指将一个容器(如列表、元组或字典)的内容拆分并分配给多个变量或作为参数传递给函数。
  • Python 提供了简洁的语法来实现这一点,使用 *** 分别解包可迭代对象和字典。

*args 和 **kwargs

在 Python 中,*args**kwargs 是非常强大的工具,用于处理可变数量的参数。它们使得函数可以接收任意数量的位置参数和关键字参数,并将这些参数传递给其他函数或方法。让我们详细解释一下你提供的代码片段:

1
2
def __call__(self, *input, **kwargs):
result = self.forward(*input, **kwargs)

为什么使用 *** 可以传递参数

  1. 收集参数
  • *input:收集所有未命名的位置参数(非关键字参数),并将它们打包成一个元组。
  • **kwargs:收集所有未明确列出的关键字参数,并将它们打包成一个字典。
  1. 解包参数
  • 在调用 self.forward 时,*input 将之前收集的位置参数解包为单独的参数传递给 forward 方法。
  • 同样,**kwargs 将之前收集的关键字参数解包为单独的关键字参数传递给 forward 方法。

具体工作原理

当你调用 __call__ 方法时,你可以传递任意数量的位置参数和关键字参数。例如:

1
2
obj = SomeClass()
obj(1, 2, 3, key1='value1', key2='value2')

在这个例子中:

  • 1, 2, 3 被收集到 *input 中,形成元组 (1, 2, 3)
  • key1='value1', key2='value2' 被收集到 **kwargs 中,形成字典 {'key1': 'value1', 'key2': 'value2'}

解包可迭代对象

函数定义中的 *

  • 位置参数收集

  • 在函数定义中,*args 用于收集所有未命名的位置参数(非关键字参数),并将它们打包成一个元组。

  • 强制关键字参数

  • 如果在参数列表中使用了单独的 *,那么 * 后面的所有参数必须以关键字形式传递。

示例

1
2
3
4
5
def example_function(a, b, *args):
print(f"a: {a}, b: {b}")
print("Additional positional arguments:", args)

example_function(1, 2, 3, 4, 5)

输出:

1
2
a: 1, b: 2
Additional positional arguments: (3, 4, 5)

强制关键字参数

1
2
3
4
5
6
7
8
def another_function(a, b, *, x, y):
print(f"a: {a}, b: {b}, x: {x}, y: {y}")

# 下面的调用会报错,因为 x 和 y 必须是关键字参数
# another_function(1, 2, 3, 4)

# 正确的调用方式
another_function(1, 2, x=3, y=4)

输出:

1
a: 1, b: 2, x: 3, y: 4

函数调用中的 *

1
2
3
4
5
6
def sum_three_numbers(x, y, z):
return x + y + z

numbers = [1, 2, 3]
result = sum_three_numbers(*numbers)
print(result) # 输出:6

解包字典

  • 在函数定义中,** 用于将传入的关键字参数打包成一个字典;
  • 而在函数调用中,** 则用于将字典解包为关键字参数。

函数定义中的 **kwargs

当你在函数定义中使用 **kwargs 时,所有未明确列出的关键字参数都会被收集到一个名为 kwargs 的字典中。

1
2
3
4
5
def example_function(a, b, **kwargs):
print(f"a: {a}, b: {b}")
print("Additional arguments:", kwargs)

example_function(1, 2, x=3, y=4)

输出:

1
2
a: 1, b: 2
Additional arguments: {'x': 3, 'y': 4}

函数调用中的 ** 解包字典

当你在函数调用中使用 ** 时,它会将字典中的键值对解包为关键字参数传递给函数。这意味着字典的键会成为参数名,对应的值会成为参数值。

1
2
3
4
5
def another_function(a, b, x, y):
print(f"a: {a}, b: {b}, x: {x}, y: {y}")

args_dict = {'x': 3, 'y': 4}
another_function(1, 2, **args_dict)

输出:

1
a: 1, b: 2, x: 3, y: 4

在这个例子中,args_dict 是一个字典,包含键 xy 及其对应的值。通过 **args_dict,这些键值对被解包为关键字参数传递给 another_function

DEBUG

段错误

  1. 开启 Python 的调试模式
    通过设置环境变量启用 Python 的调试信息,这有助于捕获异常和详细的堆栈信息。

    1
    export PYTHONMALLOC=debug
  2. 使用 faulthandler 模块
    Python 提供了一个 faulthandler 模块,可以用来捕获段错误并打印堆栈信息。你可以在程序的开头添加以下代码来启用它:

    1
    2
    import faulthandler
    faulthandler.enable()

    这将会在段错误发生时输出堆栈跟踪。

  3. 查看 Python 调试输出
    启动 Python 程序时,通过 faulthandler 打印堆栈信息,或通过 GDB 调试 Python 解释器。如果 Python 解释器发生崩溃,faulthandler 会帮助你定位错误。

doctest

函数的单元测试

打印当前堆栈

traceback.print_stack()

VizTracer时间性能分析

1
2
3
4
5
6
7
8
9
10
from viztracer import VizTracer

tracer = VizTracer(max_stack_depth=2) # 限制记录的调用栈深度为2,常用为 50和120
tracer.start()

# 你的代码
your_function()

tracer.stop()
tracer.save("result.json")

icecream for debug

rich 库是icecream的上位替代

  • rich:功能更全:支持任意对象的详细信息,包括method; 支持log;支持进度条;支持打印堆栈。
  • rich:打印更华丽

pprint 也不错

pprint 是 Python 的 pprint 模块中的一个函数,全称是 pretty-print(漂亮打印)。它用于以更易读的格式打印数据结构,如字典、列表等。

1
2
from pprint import pprint
pprint(obj)
  • 优雅打印对象:函数名,结构体
  • 打印行号和栈(没用输入时
  • 允许嵌套(会将输入传递到输出
  • 允许带颜色ic.format(*args)获得ic打印的文本
  • debug ic.disable()and ic.enable()
  • 允许统一前缀 ic.configureOutput(prefix='Debug | ')
  • 不用每个文件import
1
2
3
4
5
6
7
from icecream import ic
ic(STH)

from icecream import install
install()

ic.configureOutput(prefix='Debug -> ', outputFunction=yellowPrint)

icecream 是实时打印

普通print不是实时的,可能会出现,代码顺序在后面的ic反而打印在print前面。为此需要print(xxx,flush=True)

prefix 打印代码位置和时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import datetime
import inspect
from icecream import ic

def ic_with_timestamp(*args):
# Get the current frame's information
frame = inspect.currentframe().f_back # Get the caller's frame
filename = frame.f_code.co_filename # File where the function is called
lineno = frame.f_lineno # Line number where the function is called

timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

# Format the output to include timestamp, file, and line number
return '\n\n%s %s:%d shaojieLog >| ' % (timestamp, filename, lineno)

# Configure icecream to use this custom output function
ic.configureOutput(prefix=ic_with_timestamp)

# Example usage
ic("This is a test message.")

prefix 添加时间

1
2
3
4
5
import datetime
def ic_with_timestamp(*args):
return '\n\n%s shaojieLog >| ' % datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

ic.configureOutput(prefix=ic_with_timestamp)

打印ic间时间间隔

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
import datetime
import inspect
from icecream import ic

# Initialize a variable to store the time of the last ic call
last_ic_time = None
initial_ic_time = datetime.datetime.now() # Set the initial time when the script starts

# Define a custom function that prepends the time since the last ic call, file name, and line number
def ic_with_timestamp(*args):
global last_ic_time
current_time = datetime.datetime.now()

# Calculate the time difference if there was a previous ic call
if last_ic_time is not None:
time_diff = current_time - last_ic_time
time_diff_str = f" (+{time_diff.total_seconds():.2f}s)"
else:
time_diff_str = ""

# Calculate the time since the initial call
time_since_initial = current_time - initial_ic_time
time_since_initial_str = f" [Total time: {time_since_initial.total_seconds():.2f}s]"

# Update last_ic_time to the current time
last_ic_time = current_time

return f'\n\n{current_time.strftime("%Y-%m-%d %H:%M:%S")}{time_diff_str}{time_since_initial_str} shaojieLog |> '
ic.configureOutput(prefix=ic_with_timestamp)

torchrun等多进程环境,利用dist.rank==0来保证只有一个打印

1
2
3
4
5
6
7
8
# Disable icecream
ic.disable()

# This message will be hidden
ic("This message will NOT be shown")

# Re-enable icecream
ic.enable()

ic()的输出无法被tee的log文件捕获

这个问题与 icecream 库的 ic() 函数的默认输出机制有关。icecream 默认将输出发送到标准错误(stderr),而 tee 命令的默认行为是只捕获标准输出(stdout)。因此,ic() 的输出不会被 tee 捕获到。

要解决这个问题,你可以采取以下几种方式:

  1. 使用 ic() 输出到标准输出
  2. 你可以配置 icecream 的输出流,使其输出到标准输出,而不是默认的标准错误。这样,tee 就可以捕获 ic() 的输出。
1
2
3
4
from icecream import ic
import sys

ic.configureOutput(outputFunction=sys.stdout.write)

这样,ic() 的输出就会被发送到标准输出,然后可以被 tee 命令捕获到。

  1. tee 捕获标准错误和标准输出

你也可以让 tee 捕获标准错误(stderr)和标准输出(stdout),这样无需修改 icecream 的配置。

在你的命令中,可以使用如下方式:

1
python3.8 setup.py build bdist_wheel 2>&1 | tee compile.log

在这个命令中,2>&1 将标准错误重定向到标准输出,因此 tee 可以同时捕获两者。

  1. 使用 tee 捕获标准错误单独输出
如果你只想捕获标准错误的输出,并将其保存到日志文件,可以使用以下命令:

1
python3.8 setup.py build bdist_wheel 1>&2 | tee compile.log
或将 `stderr` 和 `stdout` 单独重定向:
1
python3.8 setup.py build bdist_wheel 2>compile.log

性能优化 与 可视化

定位 Python 中 setup.py 脚本运行缓慢的 热点,可以通过多种方式进行性能分析,具体步骤取决于你想了解的性能细节。以下是几种常见的方法来定位性能瓶颈。

方法 1: 使用 cProfile 进行性能分析

cProfile 是 Python 标准库中用于进行性能分析的工具。你可以用它来跟踪 setup.py 执行时的函数调用并找到性能瓶颈。

cProfile + snakeviz + gprof2dot

1
./gprof2dot.py -f pstats Diff.status | dot -Tpng -o ./output/Diff.png

1.1 使用 cProfile 分析 setup.py

你可以通过 cProfile 运行 setup.py 并生成分析报告:

1
python -m cProfile -o setup.prof setup.py install

这将运行 setup.py 并将性能分析结果保存到 setup.prof 文件中。

1.2 可视化分析报告

使用 pstats 或者第三方工具 snakeviz 来分析 setup.prof

  1. 使用 pstats 来查看分析结果:

    1
    python -m pstats setup.prof

    然后,你可以在 pstats 交互式界面中输入命令,比如:

    • sort cumtime 按总耗时排序。
    • stats 查看函数调用的分析结果。
  2. 安装 snakeviz 来生成Web图形化报告:

    1
    pip install snakeviz

    运行 snakeviz 来可视化分析结果:

    1
    snakeviz setup.prof # deploy to 127.0.0.1:8080

    这样可以生成一个图形化的界面,显示每个函数的执行时间以及调用关系,让你更直观地看到性能瓶颈。

  3. 使用 gprof2dot 生成调用关系图片:

    安装 gprof2dot 工具:pip install gprof2dot

    使用 gprof2dot 将 cProfile 生成的 output.prof 转换为 .dot 文件:gprof2dot -f pstats output.prof | dot -Tsvg -o output.svg

    这里的 -f pstats 表示输入的格式是 cProfile 生成的 pstats 文件。这个命令会将结果转换为 SVG 格式的火焰图,保存为 output.svg。

    打开生成的 SVG 文件,查看火焰图。

  4. 生成火焰图: flameprof

    1. 正常的火焰图说明了上到下的调用关系,倒置火焰图说明了底层最耗时的元素。
    2. python flameprof.py input.prof > output.svg
  5. 生成火焰图(有详细文件路径): flamegraph

    1. flameprof --format=log requests.prof | xxx_path/flamegraph.pl > requests-flamegraph.svg

方法 3: 使用 line_profiler 进行逐行性能分析

如果你想深入了解 setup.py 的某个函数或一组函数的逐行性能,可以使用 line_profiler 工具来分析代码的逐行执行时间。

3.1 安装 line_profiler

1
pip install line_profiler

3.2 添加装饰器

首先,在 setup.py 中找到你想要分析的函数,添加 @profile 装饰器(在 line_profiler 中的分析模式下使用):

1
2
3
@profile
def some_function():
# Your function code

3.3 运行 line_profiler

你可以使用 kernprof.py 来运行 setup.py 并生成逐行性能报告:

1
kernprof -l -v setup.py install

这将运行 setup.py 并生成一份逐行性能分析报告,显示每一行代码的耗时。

方法 4: 使用 Py-Spy 进行实时性能分析(推荐!!!)

Py-Spy 是一个 Python 的取样分析器,它可以在不修改代码的情况下对 Python 程序进行性能分析,并生成实时的性能报告。

py-spy top — xxx 有时会卡住

4.1 安装 Py-Spy

1
pip install py-spy

4.2 运行 Py-Spysetup.py 进行分析

你可以在执行 setup.py 的同时运行 Py-Spy 进行取样分析:

1
py-spy top -- python setup.py install

这会生成一个实时的报告,类似于 top 命令,显示当前正在运行的 Python 函数以及其消耗的 CPU 时间。

4.3 生成火焰图

如果你希望生成一个更直观的火焰图,可以使用 py-spy 生成火焰图文件:

1
py-spy record -o profile.svg -- python setup.py install

然后你可以打开 profile.svg 文件,查看一个交互式的火焰图,清晰展示函数调用的时间分布。

方法 5: 使用 strace 分析系统调用

如果 setup.py 涉及大量的 I/O 操作(比如读写文件或安装依赖包),可能是这些操作导致了性能瓶颈。你可以使用 strace 来分析 setup.py 的系统调用,找到 I/O 操作的瓶颈。

1
strace -tt -T -o strace.log python setup.py install
  • -tt 选项会显示每个系统调用的时间戳。
  • -T 会显示每个系统调用耗时。
  • -o 将结果输出到 strace.log 文件中。

通过查看 strace.log,你可以找出系统调用中哪些操作耗时过长。


总结

  1. 使用 cProfilePy-Spy 进行函数级别的性能分析,找出执行慢的函数。
  2. 如果需要更细粒度的逐行分析,使用 line_profiler 来分析慢的部分。
  3. 如果怀疑是 I/O 问题,用 strace 来检查系统调用。
  4. 使用 time 在脚本中插入计时代码,快速定位长时间的执行步骤。

这些工具可以帮助你定位和修复 setup.py 运行缓慢的热点。

虚拟环境venv

1
2
3
4
5
6
7
8
python3 -m venv name

#在Windows上,运行:
name\Scripts\activate.bat # poweshell运行activate.ps1
#在Unix或MacOS上,运行:
source name/bin/activate
#(这个脚本是为bash shell编写的。如果你使用 csh 或 fish shell,你应该改用 activate.csh 或 activate.fish 脚本。)
python3 setup.py install

实践

  1. 并行调用shell命令,超时kill
  2. 基于Pipe的自定义多进程进度条

数据快速写入和读取文件

任意变量使用pickle

1
2
3
4
5
# 使用二进制
with open('my_dict.json', 'wb') as f:
pickle.dump(my_dict, f)
with open('my_dict.json', 'rb') as f:
loaded_dict = pickle.load(f)

可以序列化的使用json

1
2
3
4
5
6
7
8
import json
# 将 dict 保存为 JSON 格式
with open('my_dict.json', 'w') as f:
json.dump(my_dict, f)

# 加载 dict
with open('my_dict.json', 'r') as f:
loaded_dict = json.load(f)

多个变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 将多个变量组织成字典或列表
data = {
"scaAvgTime": scaAvgTime,
"var2": var2,
"var3": var3
}

result_file = "result.json"

# 将数据写入JSON文件
with open(result_file, "w") as f:
json.dump(data, f)

# 读取JSON文件
with open(result_file, "r") as f:
data = json.load(f)

# 获取保存的变量值
scaAvgTime = data["scaAvgTime"]
var2 = data["var2"]
var3 = data["var3"]

参考文献

https://zhuanlan.zhihu.com/p/39259061

Inline Assembly

GCC内联汇编

1
__asm__ __volatile__("Instruction List" : Output : Input : Clobber/Modify);
  1. __asm__或asm 用来声明一个内联汇编表达式,所以任何一个内联汇编表达式都是以它开头的,是必不可少的。
  2. __volatile__或volatile 是可选的。如果用了它,则是向GCC 声明不允许对该内联汇编优化,否则当 使用了优化选项(-O)进行编译时,GCC 将会根据自己的判断决定是否将这个内联汇编表达式中的指令优化掉。
  3. Instruction List 是汇编指令序列。它可以是空的,比如:__asm__ __volatile__(""); 或 __asm__ ("");都是完全合法的内联汇编表达式,只不过这两条语句没有什么意义。
    1. 但并非所有Instruction List 为空的内联汇编表达式都是没有意义的,比如:__asm__ ("":::"memory");就非常有意义,它向GCC 声明:“内存作了改动”,GCC 在编译的时候,会将此因素考虑进去。
    2. 当在”Instruction List”中有多条指令的时候,需要用分号(;)或换行符(\n)将它们分开。
    3. 指令中的操作数可以使用占位符引用C语言变量,操作数占位符最多10个,名称如下:%0,%1,…,%9。指令中使用占位符表示的操作数,总被视为long型(4个字节),
      1. 但对其施加的操作根据指令可以是字或者字节,当把操作数当作字或者字节使用时,默认为低字或者低字节
      2. 对字节操作可以显式的指明是低字节还是次字节。方法是在%和序号之间插入一个字母,”b”代表低字节,”h”代表高字节,例如:%h1
  4. Output/Input
    1. 格式为形如"constraint"(variable)的列表(逗号分隔)。按照出现的顺序分别与指令操作数”%0”,”%1”对应
    2. 每个输出操作数的限定字符串必须包含”=”表示他是一个输出操作数。例子"=r" (value)
  5. Clobber/Modify(由逗号格开的字符串组成)
    1. 在Input/Output操作表达式所指定的寄存器,或当你为一些Input/Output操作表达式使用”r”约束,让GCC为你选择一个寄存器时,GCC知道这些寄存器是被修改的,你根本不需要在Clobber/Modify域再声明它们。
    2. 但是对于”Instruction List”中的临时寄存器,需要在Clobber/Modify域声明这些寄存器或内存,让GCC知道修改了他们
      1. 例子:__asm__ ("mov R0, #0x34" : : : "R0");寄存器R0出现在”Instruction List中”,并且被mov指令修改,但却未被任何Input/Output操作表达式指定,所以你需要在Clobber/Modify域指定”R0”,以让GCC知道这一点。
    3. Clobber/Modify域存在”memory”,那么GCC会保证在此内联汇编之前,如果某个内存的内容被装入了寄存器,那么在这个内联汇编之后,如果需要使用这个内存处的内容,就会直接到这个内存处重新读取,而不是使用被存放在寄存器中的拷贝。因为这个 时候寄存器中的拷贝已经很可能和内存处的内容不一致了。

输入输出与指令的对应关系

寄存器约束符Operation Constraint

每一个Input和Output表达式都必须指定自己的操作约束Operation Constraint,这里将讨论在80386平台上所可能使用的操作约束。

当前的输入或输出需要借助一个寄存器时,需要为其指定一个寄存器约束,可以直接指定一个寄存器的名字。

常用的寄存器约束的缩写

约束 意义
r 表示使用一个通用寄存器,由 GCC 在%eax/%ax/%al,%ebx/%bx/%bl,%ecx/%cx/%cl,%edx/%dx/%dl中选取一个GCC认为合适的。
g 表示使用任意一个寄存器,由GCC在所有的可以使用的寄存器中选取一个GCC认为合适的。
q 表示使用一个通用寄存器,和约束r的意义相同。
a 表示使用%eax/%ax/%al
b 表示使用%ebx/%bx/%bl
c 表示使用%ecx/%cx/%cl
d 表示使用%edx/%dx/%dl
D 表示使用%edi/%di
S 表示使用%esi/%si
f 表示使用浮点寄存器
t 表示使用第一个浮点寄存器
u 表示使用第二个浮点寄存器
分类 限定符 描述
通用寄存器 “a” 将输入变量放入eax 这里有一个问题:假设eax已经被使用,那怎么办?其实很简单:因为GCC 知道eax 已经被使用,它在这段汇编代码的起始处插入一条语句pushl %eax,将eax 内容保存到堆栈,然 后在这段代码结束处再增加一条语句popl %eax,恢复eax的内容
“b” 将输入变量放入ebx
“c” 将输入变量放入ecx
“d” 将输入变量放入edx
“s” 将输入变量放入esi
“d” 将输入变量放入edi
“q” 将输入变量放入eax,ebx,ecx,edx中的一个
“r” 将输入变量放入通用寄存器,也就是eax,ebx,ecx,edx,esi,edi中的一个
“A” 把eax和edx合成一个64 位的寄存器(use long longs)
内存 “m” 内存变量
“o” 操作数为内存变量,但是其寻址方式是偏移量类型, 也即是基址寻址,或者是基址加变址寻址
“V” 操作数为内存变量,但寻址方式不是偏移量类型
“ “ 操作数为内存变量,但寻址方式为自动增量
“p” 操作数是一个合法的内存地址(指针)
寄存器或内存 “g” 将输入变量放入eax,ebx,ecx,edx中的一个 或者作为内存变量
“X” 操作数可以是任何类型
立即数 “I” 0-31之间的立即数(用于32位移位指令)
“J” 0-63之间的立即数(用于64位移位指令)
“N” 0-255之间的立即数(用于out指令)
“i” 立即数
“n” 立即数,有些系统不支持除字以外的立即数, 这些系统应该使用”n”而不是”i”
匹配 “ 0 “,“1” …“9” , 表示用它限制的操作数与某个指定的操作数匹配,也即该操作数就是指定的那个操作数,例如”0”去描述”%1”操作数,那么”%1”引用的其实就是”%0”操作数,注意作为限定符字母的0-9 与 指令中的”%0”-“%9”的区别,前者描述操作数,后者代表操作数。
&; 该输出操作数不能使用过和输入操作数相同的寄存器
操作数类型 “=” 操作数在指令中是只写的(输出操作数)
“+” 操作数在指令中是读写类型的(输入输出操作数)
浮点数 “f” 浮点寄存器
“t” 第一个浮点寄存器
“u” 第二个浮点寄存器
“G” 标准的80387浮点常数
% 该操作数可以和下一个操作数交换位置.例如addl的两个操作数可以交换顺序 (当然两个操作数都不能是立即数)
# 部分注释,从该字符到其后的逗号之间所有字母被忽略
* 表示如果选用寄存器,则其后的字母被忽略

内存约束

如果一个Input/Output 操作表达式的C/C++表达式表现为一个内存地址,不想借助于任何寄存器,则可以使用内存约束。比如:

1
2
__asm__("lidt%0":"=m"(__idt_addr));
__asm__("lidt%0"::"m"(__idt_addr));
修饰符 输入/输出 意义
= O 表示此Output操作表达式是Write-Only的。
  •            | O               |表示此Output操作表达式是Read-Write的。
    

& | O |表示此Output操作表达式独占为其指定的寄存器。
% | I |表示此Input 操作表达式中的C/C++表达式可以和下一 个Input操作表达式中的C/C++表达式互换

例子

1
2
3
4
5
6
7
Static __inline__ void __set_bit(int nr, volatile void * addr)
{
__asm__(
"btsl %1,%0"
:"=m" (ADDR)
:"Ir" (nr));
}

第一个占位符%0与C 语言变量ADDR对应,第二个占位符%1与C语言变量nr对应。因此上面的汇编语句代码与下面的伪代码等价:btsl nr, ADDR

Clobber/Modify域存在”memory”的其他影响

使用”memory”是向GCC声明内存发生了变化,而内存发生变化带来的影响并不止这一点。

例如:

1
2
3
4
5
6
7
8
9
int main(int __argc, char* __argv[]) 
{
int* __p = (int*)__argc;
(*__p) = 9999;
__asm__("":::"memory");
if((*__p) == 9999)
return 5;
return (*__p);
}

本例中,如果没有那条内联汇编语句,那个if语句的判断条件就完全是一句废话。GCC在优化时会意识到这一点,而直接只生成return 5的汇编代码,而不会再生成if语句的相关代码,而不会生成return (*__p)的相关代码。

但你加上了这条内联汇编语句,它除了声明内存变化之外,什么都没有做。

但GCC此时就不能简单的认为它不需要判断都知道 (*__p)一定与9999相等,它只有老老实实生成这条if语句的汇编代码,一起相关的两个return语句相关代码。

另外在linux内核中内存屏障也是基于它实现的include/asm/system.h中

1
# define barrier() _asm__volatile_("": : :"memory")

主要是保证程序的执行遵循顺序一致性。呵呵,有的时候你写代码的顺序,不一定是终执行的顺序,这个是处理器有关的。

Linux 源码例子

1
2
3
4
5
6
7
8
9
10
static inline char * strcpy(char * dest, const char *src)
{
char *xdest = dest;
__asm__ __volatile__
("1: \tmoeb %1@+, %0@+\n\t" "jne 1b" //这个冒号不是分隔符
: "=a" (dest) , "=a" (stc)
: "0"(dest), "1" (src)
: "memory");
return xdest;
}

需要进一步的研究学习

暂无

遇到的问题

暂无

开题缘由、总结、反思、吐槽~~

参考文献

https://blog.csdn.net/yi412/article/details/80846083

https://www.cnblogs.com/elnino/p/4313340.html