Disassembly file: objdump

objdump file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Disassembly of section .plt:

0000000000402020 <.plt>:
402020: ff 35 e2 bf 02 00 pushq 0x2bfe2(%rip) # 42e008 <_GLOBAL_OFFSET_TABLE_+0x8>
402026: ff 25 e4 bf 02 00 jmpq *0x2bfe4(%rip) # 42e010 <_GLOBAL_OFFSET_TABLE_+0x10>
40202c: 0f 1f 40 00 nopl 0x0(%rax)

0000000000402030 <_Znam@plt>:
402030: ff 25 e2 bf 02 00 jmpq *0x2bfe2(%rip) # 42e018 <_Znam@GLIBCXX_3.4>
402036: 68 00 00 00 00 pushq $0x0
40203b: e9 e0 ff ff ff jmpq 402020 <.plt>

0000000000402040 <_ZNSo3putEc@plt>:
402040: ff 25 da bf 02 00 jmpq *0x2bfda(%rip) # 42e020 <_ZNSo3putEc@GLIBCXX_3.4>
402046: 68 01 00 00 00 pushq $0x1
40204b: e9 d0 ff ff ff jmpq 402020 <.plt>

.plt节主要实现了使用过程链接表(Procedure Linkage Table)实现延迟绑定的功能。

openmp相关的执行

omp_outlined函数

问题:objdump 程序 有许多 类似 <.omp_outlined..16>: 的函数,但是main函数里并没有调用。实际openmp是怎么执行这些代码的呢?

在使用了OpenMP指令的C/C++程序编译后,编译器会自动生成一些名为.omp_outlined.的函数。这些函数是OpenMP所需要的运行时支持函数,不是直接在main函数中调用的,其执行方式主要有以下几种:

  1. 一些关键的运行时函数,如线程创建、同步等,是在程序启动时由OpenMP运行时库初始化和直接调用的。
  2. 对于并行region,编译器会在主线程发起并行时调用对应的.omp_outlined.函数创建线程并发布工作。
  3. 一些工作分配,同步等调用也是隐式通过运行时库的支持函数实现的。
  4. 对于并行循环,编译器会将循环体移动到.omp_outlined.函数,在循环分配工作时调用。
  5. 减少指令,作用域变化等也会引发这些函数的调用。

所以.omp_outlined.函数的执行是隐式通过运行时库触发和调度的,不需要用户代码直接调用。它们是OpenMP实现所必须的,由编译器和运行时库协调完成。用户只需要编写OpenMP指令,不必关心具体的调用细节。

总体来说,这是一种让并行执行透明化的实现机制,减少了用户的工作量。

OpenMP的汇编代码

不同平台不同,有GOMP_parallel_start开头的。也有如下x86平台的

1
2
3
4
5
6
7
8
9
10
11
405854:	48 c7 84 24 a0 00 00 	movq   $0x4293b9,0xa0(%rsp)
40585b: 00 b9 93 42 00
405860: 48 8d bc 24 90 00 00 lea 0x90(%rsp),%rdi
405867: 00
405868: ba 10 5f 40 00 mov $0x405f10,%edx
40586d: be 02 00 00 00 mov $0x2,%esi
405872: 4c 89 f9 mov %r15,%rcx
405875: 4c 8b 44 24 20 mov 0x20(%rsp),%r8
40587a: 31 c0 xor %eax,%eax
40587c: e8 ff cb ff ff callq 402480 <__kmpc_fork_call@plt>
405881: 48 8b 7c 24 60 mov 0x60(%rsp),%rdi

这段汇编代码实现了OpenMP中的并行构造,主要执行了以下几个步骤:

  1. 在栈上写入一个常量0x4293b9,可能是team的参数 (48 c7 84 24)
  2. 准备参数,获取rsp+0x90地址到rdi作为第1参数 (%rdi)
  3. 设置edx为0x405f10,可能是kmp_routine函数地址
  4. esi设置为2,可能表示有2个参数
  5. r15设置到rcx,传入线程号参数
  6. r8传入栈上第0x20个参数,可能是void* shareds参数
  7. 清空eax,一些调用约定使用
  8. 调用 __kmpc_fork_call函数,这是OpenMP的runtime库函数,用来并行执行一个函数
    1. kmpc fork multiple parallel call?
  9. 最后将返回值保存在rdi指定的栈空间上

所以这段代码实现了调用OpenMP runtime并行执行一个函数的操作,准备参数,调用runtime API,获取返回值的一个流程。

利用runtime库的支持函数可以实现汇编级别的OpenMP并行性。

readelf

各section位置以及含义,参考文档

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
$ readelf -S bfs.inj
There are 37 section headers, starting at offset 0xbe8e8:
在文件内 0xbe8e8字节开始

Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
序号 节名称 节类型 节的虚拟地址偏移量 节在文件中的偏移量
节大小 每个条目的大小(如果大小固定) 节的标志 节的链接信息 节的额外信息 节的信息对齐方式
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 00000000004002a8 000002a8
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.gnu.build-i NOTE 00000000004002c4 000002c4
0000000000000024 0000000000000000 A 0 0 4
[ 3] .note.ABI-tag NOTE 00000000004002e8 000002e8
0000000000000020 0000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000400308 00000308
000000000000005c 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 0000000000400368 00000368
00000000000007e0 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 0000000000400b48 00000b48
0000000000000b1d 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 0000000000401666 00001666
00000000000000a8 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 0000000000401710 00001710
0000000000000110 0000000000000000 A 6 5 8
[ 9] .rela.dyn RELA 0000000000401820 00001820
00000000000000f0 0000000000000018 A 5 0 8
[10] .rela.plt RELA 0000000000401910 00001910
00000000000006c0 0000000000000018 AI 5 24 8
[11] .init PROGBITS 0000000000402000 00002000
000000000000001b 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 0000000000402020 00002020
0000000000000490 0000000000000010 AX 0 0 16
[13] .text PROGBITS 00000000004024b0 000024b0
0000000000026475 0000000000000000 AX 0 0 16
[14] .fini PROGBITS 0000000000428928 00028928
000000000000000d 0000000000000000 AX 0 0 4
[15] .rodata PROGBITS 0000000000429000 00029000
0000000000001180 0000000000000000 A 0 0 16
[16] .eh_frame_hdr PROGBITS 000000000042a180 0002a180
00000000000002ac 0000000000000000 A 0 0 4
[17] .eh_frame PROGBITS 000000000042a430 0002a430
0000000000001780 0000000000000000 A 0 0 8
[18] .gcc_except_table PROGBITS 000000000042bbb0 0002bbb0
00000000000005d0 0000000000000000 A 0 0 4
[19] .init_array INIT_ARRAY 000000000042dbc8 0002cbc8
0000000000000010 0000000000000008 WA 0 0 8
[20] .fini_array FINI_ARRAY 000000000042dbd8 0002cbd8
0000000000000008 0000000000000008 WA 0 0 8
[21] .data.rel.ro PROGBITS 000000000042dbe0 0002cbe0
00000000000001f0 0000000000000000 WA 0 0 8
[22] .dynamic DYNAMIC 000000000042ddd0 0002cdd0
0000000000000220 0000000000000010 WA 6 0 8
[23] .got PROGBITS 000000000042dff0 0002cff0
0000000000000010 0000000000000008 WA 0 0 8
[24] .got.plt PROGBITS 000000000042e000 0002d000
0000000000000258 0000000000000008 WA 0 0 8
[25] .data PROGBITS 000000000042e258 0002d258
0000000000000010 0000000000000000 WA 0 0 8
[26] .bss NOBITS 000000000042e280 0002d268
0000000000000180 0000000000000000 WA 0 0 64
[27] .comment PROGBITS 0000000000000000 0002d268
000000000000004a 0000000000000001 MS 0 0 1
[28] .debug_info PROGBITS 0000000000000000 0002d2b2
000000000002a06e 0000000000000000 0 0 1
[29] .debug_abbrev PROGBITS 0000000000000000 00057320
0000000000000a57 0000000000000000 0 0 1
[30] .debug_line PROGBITS 0000000000000000 00057d77
000000000000af9a 0000000000000000 0 0 1
[31] .debug_str PROGBITS 0000000000000000 00062d11
0000000000010328 0000000000000001 MS 0 0 1
[32] .debug_loc PROGBITS 0000000000000000 00073039
0000000000042846 0000000000000000 0 0 1
[33] .debug_ranges PROGBITS 0000000000000000 000b587f
00000000000054c0 0000000000000000 0 0 1
[34] .symtab SYMTAB 0000000000000000 000bad40
00000000000018c0 0000000000000018 35 106 8
[35] .strtab STRTAB 0000000000000000 000bc600
0000000000002177 0000000000000000 0 0 1
[36] .shstrtab STRTAB 0000000000000000 000be777
000000000000016c 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)

字段含义

  • Type 字段,具体含义参考文档1-10
  • Link 字段中的值是节头表中节头条目的索引,索引从0开始,表示第一个节头表条目,依此类推。比如5 代表与[ 5] .dynsym 有关

值得注意

One section type, SHT_NOBITS described below, occupies no
space in the file, and its sh_offset member locates the conceptual placement in the
file.

so the number “2d258” remains unchanged.

1
2
3
4
[25] .data             PROGBITS         000000000042e258  0002d258
0000000000000010 0000000000000000 WA 0 0 8
[26] .bss NOBITS 000000000042e280 0002d268
0000000000000180 0000000000000000 WA 0 0 64

.got

global offset table

.plt

This section holds the procedure linkage table. See ‘‘Special Sections’’ in Part 1 and ‘‘Procedure Linkage Table’’ in Part 2 for more information.

Function symbols (those with type STT_FUNC) in shared object files have special significance. When
another object file references a function from a shared object, the link editor automatically creates a procedure linkage table entry for the referenced symbol.

参考文档2-17 page48

需要进一步的研究学习

暂无

遇到的问题

暂无

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

参考文献

上面回答部分来自ChatGPT-3.5,没有进行正确性的交叉校验。

GNU Assembly File

GNU汇编语法

伪指令

  • 指示(Directives): 以点号开始,用来指示对编译器,连接器,调试器有用的结构信息。指示本身不是汇编指令。
伪指令 描述
.file 指定由哪个源文件生成的汇编代码。
.data 表示数据段(section)的开始地址
.text 指定下面的指令属于代码段。
.string 表示数据段中的字符串常量。
.globl main 指明标签main是一个可以在其它模块的代码中被访问的全局符号 。
.align 数据对齐指令
.section 段标记
.type 设置一个符号的属性值
  • 语法:.type name , description
    • description取值如下:
      • %function 表示该符号用来表示一个函数名
      • %object 表示该符号用来表示一个数据对象

至于其它的指示你可以忽略。

实践:阅读汇编文件

从最简单的C文件入手

1
2
3
int main(){
return 0;
}

运行gcc -S -O3 main.c -o main.s,得到main.s文件

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
 .file "simple.cpp"
.text
.section .text.startup,"ax",@progbits
.p2align 4
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
endbr64
xorl %eax, %eax
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:
  • 下面回答来自ChatGPT-3.5,暂时没有校验其可靠性(看上去貌似说得通)。

section

  • .section .rodata.str1.1,"aMS",@progbits,1
    • rodata.str1.1是一个标号(label), 意思是只读数据段的字符串常量
    • aMS是一个属性值:
      • 可分配的(allocatable),即程序运行时需要动态分配空间才能分配该代码段,
      • 不可执行 (M),
      • 数据的类型为串(S)
      • 其余属性值:对齐方式的通常为 b(byte对齐),w(word对齐),或者其他更大的对齐单位,例如 d(double word对齐)。
    • @progbits: 表示该段的类型是程序数据段(PROGBITS),这种类型的段包含程序的代码和数据。
    • 1: 表示该段的对齐方式是2^1 = 2个字节(按字节对齐)。如果不写这个数字,默认对齐到当前机器的字长。
  • .section .text.startup,"ax",@progbits 其中ax表示该段是可分配的(allocatable)和可执行的(executable)。
  • .section .note.GNU-stack“指令用于告诉链接器是否允许在堆栈上执行代码。
  • .section .note.gnu.property“指令用于指定一些属性,这里是一个GNU特性标记。

汇编的入口

  • 汇编的执行流程:入口函数在哪里
  • 入口函数在该文件中的名称为“main”,定义于“.text.startup” section,其首地址为“.globl main”。
1
2
3
4
.section .text.startup,"ax",@progbits
.p2align 4
.globl main
.type main, @function

构造函数

  • 为了确保这些初始化操作可以在程序启动时正确执行,编译器将把这些构造函数和析构函数的调用代码打包成若干个函数,统一放在名字为“_GLOBAL__sub_I_xxx”的section中。
    • 因为在C++程序编译后的二进制文件中,全局变量、静态变量和全局对象等信息都需要进行初始化操作,包括构造函数(初始化对象)和析构函数(清理对象)。
    • 在这段汇编代码中,也就是那个”_GLOBAL__sub_I_main”函数,它是C++全局变量和静态变量的构造函数,它调用了预初始化函数 “ios_base::Init()“,并注册了一个在程序退出时调用的析构函数 “__cxa_atexit“。
  • 在”.init_array“ section中,定义了一个”_GLOBAL__sub_I_main”的地址,这是在程序启动时需要调用的所有C++全局和静态对象的初始化函数列表,编译器链接这个列表并在程序启动时依次调用这些初始化函数。
  • 总之,这两个section的存在是为了保证C++全局变量和静态变量的正确初始化。

其中四条指令都定义了一些符号或变量,并分配了一些内存空间,这些在程序里的意义如下:

  1. .quad _GLOBAL__sub_I_main“:

在程序启动时,将调用所有全局静态对象的构造函数。这些构造函数被放在一个名为”_GLOBAL__sub_I_xxx”的section中,而每个section都是由一个指向该section所有对象的地址列表所引用。这里的”.quad _GLOBAL__sub_I_main”是为了将”_GLOBAL__sub_I_main”函数的地址添加到该列表中。

  1. .local _ZStL8__ioinit“:

这条指令定义了一个本地符号”_ZStL8__ioinit”,它表示C++标准输入输出的初始化过程。由于该符号是一个本地符号,所以只能在编辑该文件的当前单元中使用该符号。

  1. .comm _ZStL8__ioinit,1,1“:

这条指令定义了一个名为”_ZStL8__ioinit”的未初始化的弱符号,并为该符号分配了1个大小的字节空间。这个弱符号定义了一个C++标准输入输出部分的全局状态对象。在全用动态库时,不同的动态库可能有自己的IO状态,所以为了确保C++输入输出的状态正确,需要为其指定一个单独的段来存储这些状态数据。在这里,”.comm _ZStL8__ioinit,1,1”将会为”_ZStL8__ioinit”符号分配一个字节大小的空间。

  1. .hidden __dso_handle“:

这条指令定义了一个隐藏的符号 “__dso_handle”。这个符号是一个链接器生成的隐式变量,其定义了一个指向被当前动态库使用的全局数据对象的一个指针。该符号在被链接进来的库中是隐藏的,不会被其他库或者main函数本身调用,但是在main返回后,可以用来检查库是否已经被卸载。

末尾的元数据

这段代码是一些特殊的指令和数据,主要是用于向可执行文件添加一些元数据(metadata)。这些元数据可能包含各种信息,如调试信息、特定平台的指令集支持等等。

具体来说:

  • “.long”指令用于定义一个长整型数值,这里用来计算地址之间的差值。
    • 例如,第一行”.long 1f - 0f“建立了一个长整型数值,表示”1:”标签相对于当前指令地址(即0f)的偏移量。偏移量可以用来计算标签对应的指令地址,从而可用于跳转或计算指针偏移量。
    • 4f - 1f“,即”4:”标签相对于”1:”标签的偏移量;
  • .long 0xc0000002“表示这是一个特殊的属性标记,标识这个文件可以在Linux平台上执行。它是用来告诉操作系统这个程序是用特定指令集编译的。
  • .long 0x3“表示另一个属性标记,表示这个文件可以加载到任意地址。

总之,这些元数据可能对程序运行起到关键作用,但在大多数情况下可能都没有明显的作用,因此看起来没有用。

比较汇编的debugging symbols

执行gcc -S -g testBigExe.cpp -o testDebug.s,对比之前的汇编文件,由72行变成9760行。

  • -g前后

.loc

1
2
3
4
.LBE32:
.file 3 "/usr/include/c++/9/bits/char_traits.h"
.loc 3 342 2 is_stmt 1 view .LVU4
.loc 1 5 11 is_stmt 0 view .LVU5
  • 第一行:.loc 3 342 2 表示当前指令对应的源代码文件ID为3,在第342行,第2列(其中第1列是行号,第2列是第几个字符),同时is_stmt为1表示这条指令是语句的起始位置。
  • 第二行:.loc 1 5 11 表示当前指令对应的源代码文件ID为1,在第5行,第11列,同时is_stmt为0表示这条指令不是语句的起始位置。
  • view .LVU4 表示当前指令所处的作用域(scope)是.LVU4。作用域是指该指令所在的函数、代码块等一段范围内的所有变量和对象的可见性。在这个例子中,.LVU4 是一个局部变量作用域,因为它是位于一个C++标准库头文件中的一个函数的起始位置。

debug section

新增的这些 section 存储了 DWARF 调试信息。DWARF(Debugging With Attributed Record Formats)是一种调试信息的标准格式,包括代码中的变量、类型、函数、源文件的映射关系,以及代码的编译相关信息等等。

具体来说,这些 section 存储的内容如下:

  • .debug_info:包含程序的调试信息,包括编译单元、类型信息、函数和变量信息等。
  • .debug_abbrev:包含了 .debug_info 中使用到的所有缩写名称及其对应的含义,用于压缩格式和提高效率。
  • .debug_loc:存储每个程序变量或表达式的地址范围及其地址寄存器、表达式规则等信息。在调试时用来确定变量或表达式的值和范围。
  • .debug_aranges:存储简化版本的地址范围描述,允许调试器加速地定位代码和数据的位置。
  • .debug_ranges:存储每个编译单元(CU)的地址范围,每个范围都是一个有限开区间。
  • .debug_line:存储源代码行号信息,包括每行的文件、行号、是否为语句起始位置等信息。
  • .debug_str:包含了所有字符串,如文件名、函数名等,由于每个调试信息的数据都是字符串,因此这是所有调试信息的基础。

需要注意的是,这些 section 中的信息是根据编译器的配置和选项生成的,因此不同编译器可能会生成略有不同的调试信息。

需要进一步的研究学习

  • 在编译的过程中,哪个阶段 label会变成真实执行地址

遇到的问题

暂无

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

参考文献

https://www.cnblogs.com/zuofaqi/articles/12853734.html

https://www.cnblogs.com/zuofaqi/articles/12853734.html