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");

      }

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

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

相关推荐
qq_447663051 分钟前
《Spring日志整合与注入技术:从入门到精通》
java·开发语言·后端·spring
蜡笔小新星7 分钟前
OpenCV中文路径图片读写终极指南(Python实现)
开发语言·人工智能·python·opencv·计算机视觉
七七知享15 分钟前
2024 Qiniu 跨平台 Qt 高级开发全解析
开发语言·qt·零基础·操作系统·跨平台·qt5·精通
脏脏a30 分钟前
C 语言分支与循环:构建程序逻辑的基石
c语言·开发语言
结衣结衣.1 小时前
【Qt】带参数的信号和槽函数
开发语言·qt·c++11
冷琴19961 小时前
基于Python+Vue开发的电影订票管理系统源码+运行步骤
开发语言·vue.js·python
L Jiawen1 小时前
【Python 2D绘图】Matplotlib绘图(统计图表)
开发语言·python·matplotlib
Run_Teenage1 小时前
C语言每日一练——day_4
c语言·开发语言
SongYuLong的博客1 小时前
C# WPF 串口通信
开发语言·c#·wpf
熊峰峰2 小时前
数据结构第六节:二叉搜索树(BST)的基本操作与实现
开发语言·数据结构·c++·算法