预处理、编译、汇编和链接

 

广义上的编译过程包括预处理、狭义上的编译、汇编三个过程。

在linux用xshell时,程序员自己给出的外部实现,需要手动链接外部的.o文件。但在windows中VS(IDE)不用,因为IDE自动完成了全过程。

预处理

作用:处理源代码中的预处理指令(以 # 开头的命令),生成纯C/C++代码。
关键操作

  • 展开头文件(#include

  • 宏替换(#define

  • 条件编译(#ifdef#endif

  • 删除注释

*.h头文件 

下面是stdio.h中的前面一些内容

#pragma once      //#pragma once非标准但广泛支持,更简洁的头文件保护
#ifndef _INC_STDIO // include guard for 3rd party interop
#define _INC_STDIO  //头文件守卫(Header Guard):通过宏定义避免重复包含导致的编译错误。

#include <corecrt.h>
#include <corecrt_wstdio.h>

#pragma warning(push)
#pragma warning(disable: _UCRT_DISABLED_WARNINGS)
_UCRT_DISABLE_CLANG_WARNINGS

_CRT_BEGIN_C_HEADER
#define BUFSIZ  512
#define _NFILE      _NSTREAM_
#define _NSTREAM_   512
#define _IOB_ENTRIES 3
#define EOF    (-1)
#define _IOFBF 0x0000
#define _IOLBF 0x0040
#define _IONBF 0x0004

#define L_tmpnam   260 // _MAX_PATH
#if __STDC_WANT_SECURE_LIB__
    #define L_tmpnam_s L_tmpnam
#endif
....

由此看出头文件中仅只包含函数或变量的声明, 如函数声明、类/结构体定义、宏定义、类型别名等,但不包含函数实现(内联函数和模板例外)。也就是说头文件中的内容不会分配内存空间

*.c源文件

#include <stdio.h>
#define N 10
#define ADD(a,b) ((a)+(b))        
int main()
{
    printf("Hello world\n");
    printf("N=%d",N);
    int result = ADD(1,1);
    printf("result=%d\n",result);
    return 0;
}

*.i文件 

 gcc -E test.c  -o test.i   //这是linux中xshell的指令,下面同理

.......
# 885 "/usr/include/stdio.h" 3 4
extern int __uflow (FILE *);
extern int __overflow (FILE *, int);
# 902 "/usr/include/stdio.h" 3 4

int main()
{
    printf("Hello world\n");
    printf("N=%d",10);
    int result = ((1)+(1));
    printf("result=%d\n",result);
    return 0;                             
}

编译

作用:将预处理后的代码翻译成汇编代码.s 文件)。值得注意的是汇编代码中没有变量名,变量名被地址取代,可以从上面的result,再结合下面的-4(%rbp)可看出。
关键操作

  • 语法和语义分析

  • 生成与平台相关的汇编指令

  • 优化代码(printf优化为puts)

 *.s文件

gcc -S test.c -o test.s

.LC0:
    .string "Hello world"       # 定义字符串常量 "Hello world",标签为 .LC0
.LC1:
    .string "N=%d"             # 定义格式化字符串 "N=%d",标签为 .LC1
.LC2:
    .string "result=%d\n"      # 定义格式化字符串 "result=%d\n",标签为 .LC2

    .text                      # 代码段开始
    .globl  main               # 声明 main 函数为全局符号(可被链接器访问)
    .type   main, @function    # 定义 main 的类型为函数
main:                          # main 函数入口
.LFB0:                         # 函数开始标签(Local Function Begin)
    .cfi_startproc             # 调用帧信息(用于栈展开调试)
    endbr64                    # 对抗控制流攻击的安全指令(Intel CET 特性)

    # === 函数序言(Prologue)=== #
    pushq   %rbp               # 保存调用者的基址指针(栈帧链)
    .cfi_def_cfa_offset 16     # 调试信息:当前栈帧偏移量为 16 字节
    .cfi_offset 6, -16         # 调试信息:寄存器 %rbp 保存在 -16(%rsp) 处
    movq    %rsp, %rbp         # 设置当前栈指针为新的基址指针
    .cfi_def_cfa_register 6    # 调试信息:现在用 %rbp 计算栈帧地址
    subq    $16, %rsp          # 在栈上分配 16 字节空间(局部变量/对齐)

    # === 调用 puts("Hello world") === #
    leaq    .LC0(%rip), %rax   # 将字符串 "Hello world" 地址加载到 %rax(RIP 相对寻址)
    movq    %rax, %rdi         # 第一个参数存入 %rdi(x86-64 调用约定)
    call    puts@PLT           # 调用动态链接的 puts 函数

    # === 调用 printf("N=%d", 10) === #
    movl    $10, %esi          # 第二个参数(10)存入 %esi
    leaq    .LC1(%rip), %rax   # 加载格式化字符串 "N=%d" 地址到 %rax
    movq    %rax, %rdi         # 第一个参数(字符串地址)存入 %rdi
    movl    $0, %eax           # 清空 %eax(表示没有浮点参数)
    call    printf@PLT         # 调用 printf

    # === 局部变量操作 === #
    movl    $2, -4(%rbp)       # 将常量 2 存入栈上的局部变量(地址 %rbp-4)
    movl    -4(%rbp), %eax     # 将局部变量的值加载到 %eax
    movl    %eax, %esi         # 将值移动到 %esi 作为 printf 的第二个参数

    # === 调用 printf("result=%d\n", 2) === #
    leaq    .LC2(%rip), %rax   # 加载格式化字符串 "result=%d\n" 地址
    movq    %rax, %rdi         # 第一个参数存入 %rdi
    movl    $0, %eax           # 清空 %eax
    call    printf@PLT         # 调用 printf

    # === 函数收尾(Epilogue)=== #
    movl    $0, %eax           # 返回值 0 存入 %eax(main 函数的返回码)
    leave                      # 等同于 movq %rbp, %rsp + popq %rbp(恢复栈帧)
    .cfi_def_cfa 7, 8          # 调试信息:栈帧恢复为调用者状态
    ret                        # 返回调用者
    .cfi_endproc               # 调用帧信息结束

汇编

作用:将汇编代码转换为机器码(目标文件 .o),生成二进制指令。
关键操作

  • 解析汇编指令为操作码(Opcode)

  • 生成可重定位目标文件(Relocatable Object File)

可重定位性:代码中的地址是相对的,链接器可将其调整为最终内存地址。

重定向可以将这些标准流指向其他设备或文件,使得程序从不同的输入源读取数据或将输出发送到不同的位置。

*.o文件

gcc -c test.c -o test.o 

实际上.o文件就是一堆二进制 ,这里我们使用指令objdump -d test.o反汇编代码段(机器码 → 汇编)

0000000000000000 <main>:
   0:	f3 0f 1e fa          	endbr64 
   4:	55                   	push   %rbp
   5:	48 89 e5             	mov    %rsp,%rbp
   8:	48 83 ec 10          	sub    $0x10,%rsp
   c:	48 8d 05 00 00 00 00 	lea    0x0(%rip),%rax        # 13 <main+0x13>
  13:	48 89 c7             	mov    %rax,%rdi
  16:	e8 00 00 00 00       	call   1b <main+0x1b>
  1b:	be 0a 00 00 00       	mov    $0xa,%esi
  20:	48 8d 05 00 00 00 00 	lea    0x0(%rip),%rax        # 27 <main+0x27>
  27:	48 89 c7             	mov    %rax,%rdi
  2a:	b8 00 00 00 00       	mov    $0x0,%eax
  2f:	e8 00 00 00 00       	call   34 <main+0x34>
  34:	c7 45 fc 02 00 00 00 	movl   $0x2,-0x4(%rbp)
  3b:	8b 45 fc             	mov    -0x4(%rbp),%eax
  3e:	89 c6                	mov    %eax,%esi
  40:	48 8d 05 00 00 00 00 	lea    0x0(%rip),%rax        # 47 <main+0x47>
  47:	48 89 c7             	mov    %rax,%rdi
  4a:	b8 00 00 00 00       	mov    $0x0,%eax
  4f:	e8 00 00 00 00       	call   54 <main+0x54>
  54:	b8 00 00 00 00       	mov    $0x0,%eax
  59:	c9                   	leave  
  5a:	c3                   	ret    
//左列:机器码的十六进制表示(如 55 对应 push %rbp)

由此可知汇编语言其实是二进制的助记符 

 nm (全称 names 或 name mangler )是一个用于分析目标文件(如可执行文件、静态库、动态库等)符号表的命令行工具。它能够列出文件中定义的或引用的符号(如函数、全局变量等),帮助开发者调试程序、分析链接问题或检查二进制文件的符号信息 。

nm test.o

0000000000000000 T main      //T:已定义的函数(如 main)。
                 U printf  
                 U puts     //U:未定义的符号(需链接时解析)。

gcc test.c -o test        注意:在linux中可执行程序通常没有固定的文件扩展名,而是通过文件的 权限属性 和 内容格式 来标识是否为可执行程序。所以在这里test就是可执行程序,但是如果是Windows的话它就是test.exe格式
nm test

000000000000038c r __abi_tag                  // 只读的ABI兼容性标签(GCC 11+引入)  
0000000000004010 B __bss_start                // 未初始化数据段(.bss)的起始地址  
0000000000004010 b completed.0                // 编译器生成的静态变量,标记初始化状态  
                 w __cxa_finalize@GLIBC_2.2.5 // 弱引用:GLIBC的全局析构函数(程序退出时调用)  
0000000000004000 D __data_start               // 已初始化数据段(.data)的起始地址  
0000000000004000 W data_start                 // 弱符号,等同于__data_start(历史遗留)  
00000000000010b0 t deregister_tm_clones       // 文本段(.text)局部符号:TM(线程本地)克隆注销逻辑  
0000000000001120 t __do_global_dtors_aux      // 文本段局部符号:全局析构函数的辅助例程  
0000000000003db8 d __do_global_dtors_aux_fini_array_entry // 数据段:存储全局析构函数的指针数组  
0000000000004008 D __dso_handle               // 已初始化数据:动态共享对象(DSO)句柄  
0000000000003dc0 d _DYNAMIC                   // 数据段:动态链接信息表(用于运行时链接)  
0000000000004010 D _edata                     // 数据段结束地址(等价于.bss起始)  
0000000000004018 B _end                       // 程序内存布局的结束地址  
00000000000011c4 T _fini                      // 文本段:程序终止代码(析构逻辑入口)  
0000000000001160 t frame_dummy                // 文本段局部符号:栈帧初始化占位函数  
0000000000003db0 d __frame_dummy_init_array_entry // 数据段:初始化帧信息的指针数组  
0000000000002100 r __FRAME_END__              // 只读段:EH(异常处理)帧数据的结束标记  
0000000000003fb0 d _GLOBAL_OFFSET_TABLE_      // 数据段:全局偏移表(GOT),用于动态链接  
                 w __gmon_start__             // 弱引用:gprof性能分析工具的初始化钩子  
0000000000002020 r __GNU_EH_FRAME_HDR         // 只读段:异常处理帧头信息  
0000000000001000 T _init                      // 文本段:程序初始化代码(构造逻辑入口)  
0000000000002000 R _IO_stdin_used             // 只读段:标记GLIBC中stdin的使用(兼容性占位)  
                 w _ITM_deregisterTMCloneTable // 弱引用:事务内存(TM)克隆表注销函数  
                 w _ITM_registerTMCloneTable   // 弱引用:事务内存(TM)克隆表注册函数  
                 U __libc_start_main@GLIBC_2.34 // 未定义符号:GLIBC的入口函数(由动态链接器解析)  
0000000000001169 T main                       // 文本段:用户定义的main函数  
                 U printf@GLIBC_2.2.5         // 未定义符号:动态链接的printf函数  
                 U puts@GLIBC_2.2.5           // 未定义符号:动态链接的puts函数  
00000000000010e0 t register_tm_clones         // 文本段局部符号:TM克隆注册逻辑  
0000000000001080 T _start                     // 文本段:程序入口点(由内核调用,跳转到main)  
0000000000004010 D __TMC_END__                // 已初始化数据:事务内存(TM)相关结构的结束标记  

链接

作用:合并多个目标文件和库,解析符号引用,生成可执行文件。
关键操作

  • 地址重定位:确定函数和变量的最终内存地址

  • 符号解析:解决跨文件的函数/变量引用(如 printf

链接分为动态链接和静态链接

特性静态链接(Static Linking)动态链接(Dynamic Linking)
链接时机编译时 将库代码直接嵌入可执行文件运行时 由系统动态加载共享库(.so/.dll
文件内容可执行文件包含所有依赖的库代码,独立性强可执行文件仅保留库的引用,依赖外部共享库文件
库文件示例Windows: .lib,Linux: .a(如 libmath.aWindows: .dll,Linux: .so(如 libmath.so
strace -e openat ./program 2>&1 | grep '\.so'  //查看程序尝试打开哪些库文件。

 由/lib可知它会动态链接里面的库文件

Linux常见目录的功能:

目录名目录的功能作用
/bin(binary)存放可执行程序或脚本文件
/sys(system)存放和系统相关的文件
/dev(device)存放设备文件
/etc一般用来存放配置文件和启动脚本
/lib(library)存放系统库文件
/var(variable)存放变化很快的文件,比如日志文件
/proc(process)存放进程相关的数据
/rootroot用户的家目录
/home/{username}普通用户的家目录
/usr用于存放用户可执行程序、库、文档等资源。

gcc -static hello.c -o hello_static # 强制静态链接

当使用了强制静态链接后, 我们发现文件的大小一下子变大了不少, 由此可见静态链接虽然导致速度变快了, 但是也引发了一个问题, 可执行程序的体积体积会变得很大 

方面静态链接动态链接
文件大小大(包含库代码)小(仅保留引用)
内存占用高(库代码重复加载)低(多进程共享库)
启动速度快(无库加载开销)稍慢(需加载库)
维护性差(更新需重新编译)好(替换库文件即可)
兼容性强(不依赖系统库)弱(依赖库版本)
适用场景嵌入式、独立分发、无外置库环境通用软件、系统库、频繁更新的模块

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值