【C++】泛型编程 ⑤ ( 函数模板原理 | C++ 编译器原理 | C / C++ 编译器编译过程 | 分析 模板函数代码 汇编文件 | 编译 模板函数代码 汇编文件 | 模板函数汇编分析总结 )

文章目录

  • 一、C++ 编译器原理
    • 1、gcc 编译器简介
    • 2、C / C++ 编译器编译过程
    • 3、gcc 编译器各阶段命令
      • ① 预处理 Pre-Processing ( 预处理器 )
      • ② 编译 Compiling ( 编译器 )
      • ③ 汇编 Assembling ( 汇编器 )
      • ④ 链接 Linking ( 链接器器 )
    • 4、gcc 编译器 与 g++ 编译器 的区别
    • 5、gcc / g++ 编译器常用命令选项
  • 二、分析 模板函数代码 汇编文件
    • 1、编译 模板函数代码 汇编文件
    • 2、分析 模板函数代码 汇编文件
    • 3、模板函数代码 汇编文件 分析总结 ( 重要 )

在前面几篇博客

  • 【C++】泛型编程 ③ ( 函数模板 与 普通函数 调用规则 | 类型匹配 | 显式指定函数模板泛型类型 )
  • 【C++】泛型编程 ④ ( 函数模板 与 普通函数 调用规则 | 类型自动转换 | 类型自动转换 + 显式指定泛型类型 )

中 , 函数模板 可以与 重载的 普通函数 放在一起 , 二者之间 的调用 有 不同的优先级 ;


在一定程度上 , 说明 函数模板 和 普通函数 有着相似性 ,

在本篇博客中 分析 C++ 编译器的 函数模板 实现底层机制 ;





一、C++ 编译器原理



1、gcc 编译器简介


gcc 编译器 英文名称是 ” GNU C Compiler ” ,

  • 支持编译多种语言 , 可以解析不同的语言 , 如 : C , C++ , Java , Pascal 等语言 ;
  • 是可移植编译器 ;
    • 支持多种平台 , 如 : Linux , Windows , Mac 等 ;
    • gcc 编译器 不仅可以编译 普通的 C 语言应用程序源码 , 还能编译 Linux 内核 ;
    • 支持交叉编译 , 如 : 在 x86 硬件上编译 arm 程序 ;
  • 模块化设计 : gcc 编译器是按照模块化设计的 , 可以加入新的编程语言和新的 CPU 架构 ;

2、C / C++ 编译器编译过程


参考 【C 语言】编译过程 分析 ( 预处理 | 编译 | 汇编 | 链接 | 宏定义 | 条件编译 | 编译器指示字 ) 博客 , C 语言 程序的编译 需要经过 预处理 , 编译 , 汇编 , 链接 操作 , 分别需要使用 预处理器 , 编译器 , 汇编器 , 链接器 四个工具 ;

集成开发环境 将 预处理器 , 编译器 , 汇编器 , 链接器 四个工具 集成到了一起 ;

打开 Visual Studio 中解决方案 所在目录 , 其中就有 编译过程 中产生的大量的 中间文件 ;


3、gcc 编译器各阶段命令



① 预处理 Pre-Processing ( 预处理器 )


预处理 Pre-Processing : 展开 宏定义 , 得到预处理文件 ;

gcc Test.c -o Test.i

也可以加上 -E 选项 ;

gcc -E Test.c -o Test.i

② 编译 Compiling ( 编译器 )


编译 Compiling : 将预处理文件编译成 汇编文件 ;

gcc Test.i -o Test.S

直接从 Test.c 源码生成 汇编文件 :

gcc -S Test.c -o Test.S

③ 汇编 Assembling ( 汇编器 )


汇编 Assembling : 将 汇编文件 编译成 二进制机器码文件 ;

gcc Test.S -o Test.o

直接从 Test.c 源码生成 机器码文件 :

gcc -c Test.c -o Test.o

④ 链接 Linking ( 链接器器 )

链接 Linking : 将 二进制机器码文件 链接成 可执行文件 ;

gcc Test.o -o Test.exe

直接生成可执行文件 :

  • 生成默认的 a.exe 可执行文件命令 :
gcc Test.c
  • 指定要生成的 可执行 文件名称 命令 :
gcc Test.c -o Test.exe

编译 C++ 代码 , 将 gcc 改为 g++ 即可 ;


4、gcc 编译器 与 g++ 编译器 的区别


gcc 编译器 与 g++ 编译器 的区别如下 :

  • 语言区别 : gcc 编译器 是 C 语言编译器 , 编译后缀为 .c 的文件 ; g++ 编译器 是 C++ 编译器 , 编译后缀为 .cpp 的文件 和 后缀为 .c 的文件 , 两者都当C++文件处理 ;
  • 编译阶段区别 : 在编译阶段 , g++ 编译器 会自动链接 STL 库 , 而 gcc 必须要加一个参数 -lstdc++ ;
  • 预定义宏区别 : gcc 在编译 c 文件时 , 可用的预定义宏比较少 ;
  • 链接阶段区别 : 通常使用 g++ 来完成链接,为了统一起见,干脆 编译 / 链接 统统用g++了。
  • 语法区别 : 虽然 C++ 语言 是 C 语言 的超集 , 但是两者对语法的要求是有区别的,C++的语法规则更加严谨一些 ;

5、gcc / g++ 编译器常用命令选项


gcc / g++ 编译器常用命令选项 :

  • -o 选项 : 产生目标文件 , 可以是 .i 预处理文件、.s 汇编文件、.o 二进制机器码文件、可执行文件等 ;
  • -c 选项 : 通知 gcc 编译器 取消链接步骤 , 只生成 .o 二进制机器码文件 ;
  • -E 选项 : 只运行 C 预编译器 , 得到 .i 预处理文件 ;
  • -S 选项 : 通知 gcc 编译器产生汇编语言文件后停止编译 , 也就是只执行 前两步操作 , 产生 .i 预处理文件 和 .s 汇编语言文件 ;
  • -Wall 选项 : 打开编译器警告选项 , 如果源码有问题 , 会发出警告 ;
  • Idir 选项 : 将 dir 目录加入搜索头文件的目录路径 ;
  • -Ldir 选项 : 将 dir 目录加入搜索库的目录路径 ;
  • -llib 选项 : 链接 lib 库 ;
  • -g 选项 : 在 .o 目标文件 中嵌入调试信息 , 以便 gdb 之类的调试程序调试 ;




二、分析 模板函数代码 汇编文件



1、编译 模板函数代码 汇编文件


在 Test.c 中定义一个简单 函数模板 , 然后再 main 函数中调用该 函数模板 ,

#include "iostream"
using namespace std;

template <typename T>
T add(T a, T b) {
	cout << "调用函数模板 T add(T a, T b)" << endl;
	return a + b;
}

int main() {

	int a = 10, b = 20; 
	int c = add(a, b);
	cout << "函数模板计算结果 : c = " << c << endl;

	return 0;
}

执行

g++ -S Test.cpp -o Test.S

命令 , 生成 该 C++ 源码对应的汇编文件 ;

生成的汇编文件 Test.S 内容如下 :

	.file	"Test.cpp"
.lcomm __ZStL8__ioinit,1,1
	.def	___main;	.scl	2;	.type	32;	.endef
	.section .rdata,"dr"
	.align 4
LC0:
	.ascii "\345\207\275\346\225\260\346\250\241\346\235\277\350\256\241\347\256\227\347\273\223\346\236\234 : c = \0"
	.text
	.globl	_main
	.def	_main;	.scl	2;	.type	32;	.endef
_main:
	leal	4(%esp), %ecx
	andl	$-16, %esp
	pushl	-4(%ecx)
	pushl	%ebp
	movl	%esp, %ebp
	pushl	%ecx
	subl	$36, %esp
	call	___main
	movl	$10, -12(%ebp)
	movl	$20, -16(%ebp)
	movl	-16(%ebp), %eax
	movl	%eax, 4(%esp)
	movl	-12(%ebp), %eax
	movl	%eax, (%esp)
	call	__Z3addIiET_S0_S0_
	movl	%eax, -20(%ebp)
	movl	$LC0, 4(%esp)
	movl	$__ZSt4cout, (%esp)
	call	__ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
	movl	%eax, %edx
	movl	-20(%ebp), %eax
	movl	%eax, (%esp)
	movl	%edx, %ecx
	call	__ZNSolsEi
	subl	$4, %esp
	movl	$__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, (%esp)
	movl	%eax, %ecx
	call	__ZNSolsEPFRSoS_E
	subl	$4, %esp
	movl	$0, %eax
	movl	-4(%ebp), %ecx
	leave
	leal	-4(%ecx), %esp
	ret
	.section .rdata,"dr"
	.align 4
LC1:
	.ascii "\350\260\203\347\224\250\345\207\275\346\225\260\346\250\241\346\235\277 T add(T a, T b)\0"
	.section	.text$_Z3addIiET_S0_S0_,"x"
	.linkonce discard
	.globl	__Z3addIiET_S0_S0_
	.def	__Z3addIiET_S0_S0_;	.scl	2;	.type	32;	.endef
__Z3addIiET_S0_S0_:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$24, %esp
	movl	$LC1, 4(%esp)
	movl	$__ZSt4cout, (%esp)
	call	__ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
	movl	$__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, (%esp)
	movl	%eax, %ecx
	call	__ZNSolsEPFRSoS_E
	subl	$4, %esp
	movl	8(%ebp), %edx
	movl	12(%ebp), %eax
	addl	%edx, %eax
	leave
	ret
	.text
	.def	___tcf_0;	.scl	3;	.type	32;	.endef
___tcf_0:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$8, %esp
	movl	$__ZStL8__ioinit, %ecx
	call	__ZNSt8ios_base4InitD1Ev
	leave
	ret
	.def	__Z41__static_initialization_and_destruction_0ii;	.scl	3;	.type	32;	.endef
__Z41__static_initialization_and_destruction_0ii:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$24, %esp
	cmpl	$1, 8(%ebp)
	jne	L6
	cmpl	$65535, 12(%ebp)
	jne	L6
	movl	$__ZStL8__ioinit, %ecx
	call	__ZNSt8ios_base4InitC1Ev
	movl	$___tcf_0, (%esp)
	call	_atexit
L6:
	leave
	ret
	.def	__GLOBAL__sub_I_main;	.scl	3;	.type	32;	.endef
__GLOBAL__sub_I_main:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$24, %esp
	movl	$65535, 4(%esp)
	movl	$1, (%esp)
	call	__Z41__static_initialization_and_destruction_0ii
	leave
	ret
	.section	.ctors,"w"
	.align 4
	.long	__GLOBAL__sub_I_main
	.ident	"GCC: (i686-posix-sjlj, built by strawberryperl.com project) 4.9.2"
	.def	__ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc;	.scl	2;	.type	32;	.endef
	.def	__ZNSolsEi;	.scl	2;	.type	32;	.endef
	.def	__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_;	.scl	2;	.type	32;	.endef
	.def	__ZNSolsEPFRSoS_E;	.scl	2;	.type	32;	.endef
	.def	__ZNSt8ios_base4InitD1Ev;	.scl	2;	.type	32;	.endef
	.def	__ZNSt8ios_base4InitC1Ev;	.scl	2;	.type	32;	.endef
	.def	_atexit;	.scl	2;	.type	32;	.endef

2、分析 模板函数代码 汇编文件


.file "Test.cpp" 表示这是 Test.cpp 源码的 汇编文件 ;

.text 表示 下面是代码 ;

_main: 表示 后面是 main 函数 ;

call __Z3addIiET_S0_S0_ 调用的是 函数模板 , 下面看函数模板的 汇编内容 :

函数模板 的 函数声明 对应的汇编如下 :

LC1:
	.ascii "\350\260\203\347\224\250\345\207\275\346\225\260\346\250\241\346\235\277 T add(T a, T b)\0"
	.section	.text$_Z3addIiET_S0_S0_,"x"
	.linkonce discard
	.globl	__Z3addIiET_S0_S0_
	.def	__Z3addIiET_S0_S0_;	.scl	2;	.type	32;	.endef

这是一个模板函数的汇编版本,函数名为add,它接受两个参数,都是int类型(T在上下文中可以推断为int)。

  • .ascii "\350\260\203\347\224\250\345\207\275\346\225\260\346\250\241\346\235\277 T add(T a, T b)\0" 这行代码是一个ASCII字符串,它表示函数模板的名称和一些模板参数。这个字符串在汇编代码中可能不会直接出现,而是由编译器插入的。
  • .section .text$_Z3addIiET_S0_S0_,"x" 这行代码定义了一个section(段),其中$_Z3addIiET_S0_S0_是该section的名称。Section名称通常是由编译器生成的,用于存储特定类型的代码或数据。在这种情况下,该section包含的是add函数的实现。”x”表示该section是可执行的。
  • .linkonce discard 这个指示告诉链接器,如果该文件在其他地方被链接了,就丢弃重复的代码。这是一种优化手段,可以避免在最终的可执行文件中包含重复的代码。
  • .globl __Z3addIiET_S0_S0_ 这行代码声明了全局符号__Z3addIiET_S0_S0_。在C++中,编译器会为每个模板函数生成一个特定的符号名称,这是模板函数的实例化。
  • .def __Z3addIiET_S0_S0_; .scl 2; .type 32; .endef 这行代码定义了符号__Z3addIiET_S0_S0_,并设置了一些属性。这些属性可能是由链接器或其他工具使用的,以确定如何处理该符号。

函数模板 的 函数体内容 回应的汇编如下 :


__Z3addIiET_S0_S0_:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$24, %esp
	movl	$LC1, 4(%esp)
	movl	$__ZSt4cout, (%esp)	# 开始打印日志
	call	__ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
	movl	$__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, (%esp)
	movl	%eax, %ecx
	call	__ZNSolsEPFRSoS_E 	# 打印日志结束
	subl	$4, %esp
	movl	8(%ebp), %edx
	movl	12(%ebp), %eax
	addl	%edx, %eax
	leave
	ret
	.text
	.def	___tcf_0;	.scl	3;	.type	32;	.endef

对应的 C++ 代码如下 :

template <typename T>
T add(T a, T b) {
	cout << "调用函数模板 T add(T a, T b)" << endl;
	return a + b;
}

打印日志

cout << "调用函数模板 T add(T a, T b)" << endl;

对应的汇编内容 :

	movl	$__ZSt4cout, (%esp)	# 开始打印日志
	call	__ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
	movl	$__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, (%esp)
	movl	%eax, %ecx
	call	__ZNSolsEPFRSoS_E 	# 打印日志结束

3、模板函数代码 汇编文件 分析总结 ( 重要 )


C++ 编译器 将 函数模板 编译成了 汇编函数 call __Z3addIiET_S0_S0_ ;

如果 向 函数模板 中传入不同的函数 , 会生成 多个不同的 汇编函数 ;


C++ 编译器 编译 函数模板 时 , 不会生成能处理任意类型参数的 函数 ,

而是 通过 函数模板 , 根据 实际传入的参数类型 生成 具体的 参数类型不同 的函数 ;


如果 函数模板 和 普通函数 定义在了一起 ,

则 C++ 编译器 编译 汇编文件 时 , 就直接使用 普通函数 替代 为 函数模板 重新生成一个 函数实例 ;


C++ 编译器 通过 两次编译 实现上述效果 ;

  • 第一次编译 会对 函数模板 进行 语法分析 , 词法分析 , 句法分析 , 生成简单的 函数模板 模型 ;

  • 第二次编译 根据 调用时 传入的实际数据类型 , 产生新的 函数模型 ;

如果 调用多次 , 那么会产生多个 新的函数模型 ;


文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
扎眼的阳光的头像扎眼的阳光普通用户
上一篇 2023年12月13日
下一篇 2023年12月13日

相关推荐