程序的周边

一:程序的诞生

平时使用gcc生成可执行程序,gcc -g -Wall hello.c -o hello
整个过程涉及了预处理、编译、汇编、链接多个步骤。

1. 预处理阶段

将宏定义展开,将头文件的内容包含,生成后缀为.i的预处理文件。

gcc/g++  -E  a.c  -o  a.i 

所以为什么全局变量不能在头文件中定义,因为定义全局变量的代码会存在于所有include包含该头文件的文件中。
一般的做法,将全局变量 使用extern 声明在头文件(eg:res.h)中,res.cpp中定义,其他使用全局变量的文件包含该头文件(res.h)

关于g++和gcc在这一阶段的区别:g++ 区别于 gcc 会在部分头文件和类型定义前添加extern “C”,这个标识符的作用把标识符作用域的数据类型采用gcc去编译。

2. 编译阶段:

对源代码进行语义分析,并优化产生对应的汇编代码的过程。生成后缀.s的汇编文件

 gcc/g++  -S  a.c  -o  a.s

使用gcc 和 g++ 编译结果对比图:

编译.PNG

对比发现函数的命名方式不一样,对于g++,因为c++支持重载,所以编译器会为每个函数重新更改名字。

3. 汇编阶段

将源码翻译成可执行的指令,并生成目标文件。后缀为.o.

gcc/g++  -c  a.s  -o  a.o

汇编阶段时,gcc/g++内部都是调用as汇编命令,在这里两者是没有区别的。

4. 链接阶段

将各个目标文件包括库文件,链接成一个可执行程序,这个过程涉及 地址和空间的分配、符号解析、重定位等等,在linux下,该工作由 GNU的链接器 ld 完成。

gcc/g++ -o a a.o

我们可以使用 -v 选项查看完整和详细的gcc编译过程。

5.这里有一篇《关于gcc和g++编译器分别对c与c++文件影响》

原文链接:https://blog.csdn.net/qq_21792169/article/details/85097822
版权声明:本文为CSDN博主「HeroKern」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

二:程序的构成

linux下可执行程序大部分是elf格式文件,可以使用 readelf 查看

readelf -h test
header.PNG
readelf -S test

[21] .dynamic DYNAMIC 0000000000200dc8 00000dc8
00000000000001f0 0000000000000010 WA 6 0 8
[22] .got PROGBITS 0000000000200fb8 00000fb8
0000000000000048 0000000000000008 WA 0 0 8
[23] .data PROGBITS 0000000000201000 00001000
0000000000000010 0000000000000000 WA 0 0 8
[24] .bss NOBITS 0000000000201010 00001010
0000000000000008 0000000000000000 WA 0 0 1
[25] .comment PROGBITS 0000000000000000 00001010
0000000000000029 0000000000000001 MS 0 0 1
[26] .symtab SYMTAB 0000000000000000 00001040
0000000000000600 0000000000000018 27 43 8
[27] .strtab STRTAB 0000000000000000 00001640
0000000000000205 0000000000000000 0 0 1
[28] .shstrtab STRTAB 0000000000000000 00001845
00000000000000fe 0000000000000000 0 0 1

可以看到比较熟悉的 data text bss等。
比较常见的段:

  1. text段:代码段,用于保存可执行指令
  2. data段:初始化数据段,保存有非0初始值的全局变量和静态变量
  3. bss段:未初始化数据段,用于保存没有初始化值或初值为0的全局变量和静态变量,当程序加载时,这些变量的值会被初始化为0。
  4. debug段:用于保存调试信息。
  5. dyamic段:用于保存动态链接信息
  6. init段:用于保存进程启动时的执行程序,当进程启动时,系统会自动执行这部分代码。
    等等吧。。。

关于程序内存分区

代码段、数据段、BSS段、堆区、映射段、栈区、内核空间
参考 https://blog.csdn.net/shayne000/article/details/88547187

关于nm和ldd

nm 可以查看可执行程序或库的符号信息
ldd 可以查看可执行程序运行时依赖的库文件
参考 https://www.cnblogs.com/xiaomanon/p/4203671.html

三:关于ABI兼容

1.API和ABI的区别

参考:https://blog.csdn.net/xinghun_4/article/details/7905298

API 应用程序接口,是编程接口。编写“应用程序”时候调用的函数之类的东西。 定义了源代码和库之间的接口,因此同样的代码可以在支持这个API的任何系统中编译。
ABI 应用程序二进制接口,是二进制接口,除非你直接使用汇编语言,这种接口一般是不能直接拿来用的。允许编译好的目标代码在使用兼容ABI的系统中无需改动就能运行.

2. ABI兼容

以下整理自知乎上一个答者。

作者:Aman
    链接:https://www.zhihu.com/question/381069847/answer/1094118331
    来源:知乎
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

详细情况建议点击上述链接直接查看原答者的回答

2.1什么是二进制兼容性呢?

如1中API和ABI的说明,假设你的应用程序引用的一个库某天更新了,虽然 API 和调用方式基本没变,但你需要重新编译你的应用程序才能使用这个库,那么一般说这个库是 Source compatible;反之,如果不需要重新编译应用程序就能使用新版本的库,那么说这个库跟它之前的版本是二进制兼容的。

2.2 怎么开发二进制兼容的程序呢

由于不同的 C++ 编译器、甚至不同版本的 C++ 编译器的 Name mangling 算法可能有所不同,所以开发二进制兼容的库的时候,一般使用 extern "C" 来抑制 Name mangling

#ifdef __cplusplus 
extern "C" {
#endif

// your code here

#ifdef __cplusplus
}
#endif

在开发二进制兼容的库的时候,一定要避免使用 STL,因为不同的 C++ 编译器、不同版本的 C++ 编译器携带的 STL 不具备二进制兼容性,甚至同一个版本的 C++ 编译器用户也可能使用不同的 STL 替代自带的 STL。或者说,二进制兼容的接口应该只使用 int32、double 等基础数据类型,使用确定的 struct 甚至完全不使用 struct、只提供抽象的 handle,或者纯抽象接口。

2.3 这里有一个ABI不兼容的问题例子

参考 https://www.cnblogs.com/Keeping-Fit/p/14251144.html

四:指令集

参考 https://www.cnblogs.com/johnnyzen/p/13224632.html

指令集 被应用的指令集架构 国产芯片
CISC 复杂指令集 x86 兆芯 amd64
RISC 精简指令集 mips 龙芯
RISC 精简指令集 arm 飞腾、华为鲲鹏
RISC 精简指令集 aarch64/arm64 飞腾

你可能感兴趣的:(程序的周边)