C语言八股---预处理,编译,汇编与链接篇

前言

从多个.c文件到达一个可执行文件的四步:

预处理-->编译-->汇编-->链接

预处理

预处理过程就是预处理器处理这些预处理指令(要不然编译器完全不认识),最终会生成 main.i的文件

主要做的事情有如下几点:

  • 展开头文件
  • 展开宏
  • 条件编译
  • 删除注释
  • 添加行号等信息
  • 保留parama预处理指令
  1. 头文件展开---#include指令
    • #include <sdtio.h> 和 #include "stdio.h"
      对于<> 搜索顺序为
      • 通过GCC参数gcc-I指定的目录(注:大写的I 让我们自由指定的)。
      • 通过环境变量CINCLUDEPATH指定的目录。
      • GCC的内定目录。
        对于 " "的搜索时顺序
      • 项目当前目录(此时也可以用".../LED/led.h"方式去搜索)
      • 通过GCC参数gcc-I指定的目录。
      • 通过环境变量CINCLUDEPATH指定的目录。
      • GCC的内定目录
    • 为什么把声明放在头文件里
      • 提供一个接口 方便其他文件通过声明调用对应的函数
      • 当我们的led.c 包含了 led.h的时候 也方便编译器做类型的检查
    • 头文件多次包含会增加可执行文件的体积吗?
      只要是使用了类似#pragma once 或者#ifndef 多次包含是不会的增加可执行文件的体积的
      同样的要注意:声明不会增加可执行文件的体积
  2. 宏展开#define 宏指令
    • 宏定义最小
      #define MIN(x,y) ((x) > (y) ? (y) : (x))
      因为宏只是做了一个替换所以对于如下代码
c 复制代码
        #include <stdio.h>
        #define MIN(x,y) ((x) > (y) ? (y) : (x))
        //因为宏只是做了一个替换所以对于如下代码
        int main()
        {
            int a = 2 ;
            int b = 5;
            int c =  MIN(a++,b++);
            printf("c = %d a = %d b = %d\r\n",c,a,b); // a竟然等于4
        }

在这里可以用GNU C语法中的一些小技巧操作

c 复制代码
        #define MIN(x,y) ({\
	        typeof(x) _x = (x);\
	        typeof(y) _y = (y);\
	        _x > _y ? _x : _y;})
  • 定义一个很大的常数的时候
      #define MAX_LONG (100001000010000)UL //指定类型
  • ##连接符
      高端用法 看了好多代码都用这个 但是分析起来乱乱的,大概就是把两个字母连接到一起
c 复制代码
        #define contact(x,y)  (x##y)
        int bc = 50;
        printf("bc = %d\r\n", contact(b,c));
  • offset_of与container_of
    之前写结构体的时候写过,权当复习一下

    c 复制代码
    #define offset_of(type, member)     ((size_t)(&((type *)0)->member))
    #define container_of(type,member,ptr) (type *)((size_t) ptr - offset_of(type,member))
    struct student {
        int height;
        char * name;
    };
    int main()
    {
        struct student stu;
        stu.height = 50;
        stu.name = "123456";
        char ** tmp_name = &stu.name;
        struct student* s = &stu;
        printf("%p  %p %ld\r\n",s, tmp_name,offset_of(struct student,height));
        struct student *new_s = container_of(struct student,name,tmp_name);
        new_s->height = 60;
        printf("%d\r\n",stu.height);
    }

``

  • 宏为什么要用 do {} while(0)
      如果去看linux源码也好还是RTOS等的代码也好 会有很多时候用到do_while(0) 它的作用是什么呢
    假设我们定义了
    #define MACRO() foo(); bar()
    此时我们写了这样的伪代码
    if (condition)
    MACRO();
    else
    baz();
    // 宏展开后和我们想要的就完全不一样了 直接就出错了
    // do {}while(0)可以保证宏作为一个整体执行 此时就可以定义一些局部变量
  1. 条件编译 #ifdef等指令
  • 条件编译指令

    正常用的比较多的就是 #ifndef #define #endif这几个连用

    也有 #defined(VAR_X)之类的

  • #error指令

    如果发生错误直接中断编译过程

  1. #pragma 指令
    • #pragma pack([n]):指示结构体和联合成员的对齐方式。
    • #pragma message("string"):在编译信息输出窗口打印自己的文
      本信息。
    • #pragma warning:有选择地改变编译器的警告信息行为。
    • #pragma once:在头文件中添加这条指令,可以防止头文件多次
      编译。

编译

真要讲编译我也是不配讲的 就我们知道这是在干嘛就行了

编译就是把.c文件变成汇编文件的过程

  • 编译过程的6步
    词法分析 / 语法分析 / 语义分析 / 中间代码生成 / 汇编代码生成 / 目标代码生成
    • 语法错误: stynax error: 缺少分号 / {}没扩住 /
    • 语义错误: 类型不匹配 未定义的变量
      最终的结果就是生成.S文件
  • gcc的优化等级 gcc -O
    可以参考
    https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#Optimize-Options
  • 交叉编译
    嵌入式开发板一般是ARM架构 然后PC是x86架构
    通过交叉编译器进行程序的编译

汇编

汇编就是把汇编代码编程机器码 也就是比较熟悉的 xx.o文件了

汇编过程最终会生成以零地址为链接起始地址的可重定位目标文件

链接

把.o 文件进行组装 需要重定位 因为所有的.o文件的开头都是以0地址开头的

链接主要分为3个过程:分段组装、符号决议和重定位

  • 分段组装

    不太好讲 基本就是通过一个脚本把多个文件按照段组合到一起

  • 符号决议

    符号决议的核心就行 如果说变量/函数重名了怎么办

    • 不允许同时存在两个相同的强符号
      初始化的全局变量、函数名默认都是强符号,未初始化的全局变量默认是弱符号
      比如
c 复制代码
        // b.c
        int i;   // 未初始化 是弱符号
        int main() {
            printf("%d\r\n",i);  //i的值是20
        }
        // a.c
        int i = 20;

__attribute__关键字 可以把强符号强行转换为弱符号 attribute((weak))

  • 使用弱符号的好处

    • 自定义重名函数

      这里在嵌入式里最常见的就是中断服务函数的弱定义了

      当我们需要重新定义中断服务函数的时候 只需要保证名字很start.S的名字一致就行,链接的时候就知道链接到哪里了

    • 检查该函数是否存在

      // b.c

      #include <stdio.h>

      int global_k;

      char global_i;
      attribute ((weak)) void func()

      {

      printf("这被定义为弱符号了\r\n");

      }

      int main()

      {

      printf("%d\r\n",global_k);

      if(func)

      func(); //调用的是强符号的函数

      return 0;

      }

      // a.c

      #include <stdio.h>

      int global_k = 20;

      int global_i;

      void func()

      {

      printf("这被定义为强符号了 fun\r\n");

      }

  • 同样都是弱符号 谁体积大谁胜出

  • 重定位
    因为要把不同的文件链接到一块 所以位置就会发生变化

相关推荐
人工干智能4 小时前
科普:Python 中,字典的“动态创建键”特性
开发语言·python
LGL6030A4 小时前
算法题实战积累(3)——方块转换(C语言)
c语言·算法
初听于你5 小时前
缓存技术揭秘
java·运维·服务器·开发语言·spring·缓存
长路归期无望7 小时前
C语言小白实现多功能计算器的艰难历程
c语言·开发语言·数据结构·笔记·学习·算法
是大强8 小时前
stm32摇杆adc数据分析
开发语言
口嗨农民工8 小时前
win10默认搜索APP和window设置控制命板
linux·服务器·c语言
蓝莓味的口香糖9 小时前
【JS】什么是单例模式
开发语言·javascript·单例模式
linux kernel9 小时前
第二十三讲:特殊类和类型转换
开发语言·c++
笨蛋少年派9 小时前
JAVA基础语法
java·开发语言
渡我白衣9 小时前
深入剖析:boost::intrusive_ptr 与 std::shared_ptr 的性能边界和实现哲学
开发语言·c++·spring