文章目录

  • 1 浅谈Linux下的进程和C语言内存分布模型
    • 1.1 进程
    • 1.2 内存分布
      • 1.2.1 栈区Stack
      • 1.2.2 堆区Heap
      • 1.2.3 全局变量区
      • 1.2.4 常量区
      • 1.2.5 代码区
  • 2 gcc编译全流程
    • 第一步:预处理 Preprocessing
      • 动作1:头文件包含
      • 动作2:宏替换
      • 动作3:删除注释
      • 动作4:条件编译
    • 第二步:编译 Compilation
    • 第三步:汇编 Assemble
    • 第四步:链接 Linking

1 浅谈Linux下的进程和C语言内存分布模型

1.1 进程

进程是指可执行文件从运行到结束的整个动态的过程。

关于Linux虚拟内存管理这块会有专门的详解,敬请期待!

1.2 内存分布

1.2.1 栈区Stack

1.2.2 堆区Heap

1.2.3 全局变量区

1.2.4 常量区

1.2.5 代码区

2 gcc编译全流程

第一步:预处理 Preprocessing

  1. 我的Ubuntu当前目录有一个源文件。(请事先安装gcc编译器 – sudo apt install gcc
$ lstest.c$ cat test.c -n 1#include  2 3int main(int argc, char **argv) 4{ 5// 输出 6printf("hello world\n"); 7 8return 0; 910}
  1. 现在我们使用预处理器将源文件生成预处理文件。
$ gcc -E test.c -o test.i$ lstest.ctest.i

这个test.i文件就是生成的预处理文件,可以通过cat或者vim查看该文件的内容,发现它从短短10行突然变成了几百行甚至几千行,这是因为以下的几个预处理动作是执行命令后自动执行的结果。


动作1:头文件包含

头文件包含就是将头文件中的内容原封不动的放在目的文件中。(从几十行代码变成几万行主要是因为这个!)

我们包含头文件有以下两种格式:

#include // 从系统指定目录中寻找该头文件。#include "stdio.h"// 先从当前目录下寻找,如找不到,再去系统指定目录中寻找该文件。

一般情况下,把自己编写的一些头文件放在当前目录下,或者可以通过脚本的方法让编译器去寻找自己指定存放头文件的位置(在此不赘述);
但是尽量不要把自己编写的头文件放到系统指定目录中和C库文件混在一起,这样会造成极大的不方便。


动作2:宏替换

  • 定义:使用关键字define定义的就叫宏。
  • 作用范围:从定义处开始,到当前文件结束都有效。
  • 作用域:没有归属性,不可以成为结构体、类的对象。

例:

#include // 把出现PI的位置全部替换成后面全部内容,不管对错合理与否,也不会在预处理阶段报错,只是单纯替换。#define PI 3.1415926int main(int argc, char **argv){PI = 3;PI();get PI;PIreturn 0;}PI PI PI


宏的分类

  1. 无参的宏。
#define CALL "hello"
  1. 有参的宏(宏函数)。
#define ADD(a, b) a + b

带参宏的特点

  1. 宏的参数不可以有类型。
#include // #define ADD(int a, int b) a + b错#define ADD(a, b) a + bint main(int argc, char **argv){int num1 = 10;int num2 = 30;printf("%d\n", ADD(1, 2));printf("%d\n", ADD(num1, num2));return 0;}
$ ./a.out 340

  1. 宏不能保证参数的完整性。(解决办法:加( ))
#include #define ADD(a, b) a + b int main(int argc, char **argv){// 我们的理想结果是9printf("%d\n", 3 * ADD(1, 2));// 3 * 1 + 2 = 5printf("%d\n", ADD(1, 2) * 3);// 1 + 2 * 3 = 7return 0;}
$ ./a.out 57

查看预处理文件


  1. 带参宏和带参函数的区别

带参宏:

  • 宏的参数不能有类型。
  • 带参宏被调用多少次就会展开多少次,执行代码的时候没有函数调用的过程,不需要压栈弹栈。
  • 浪费空间来节省时间,用空间换取时间。

带参函数:

  • 形参是有类型的。
  • 带参函数,代码只有一份,存储在代码段,调用的时候去代码段取指令,调用需要压栈弹栈,存在调用的过程。
  • 用时间换取空间。

动作3:删除注释

代码中的所有注释会在这一动作下全部清除。


动作4:条件编译

条件编译的作用

  • 用于代码裁剪。
// 常用的注释方式,针对大面积代码注释#if 0代码块#endif
  • 用于防止头文件重复包含。
方法1:#ifndef __MAIN_H__#define __MAIN_H__#endif方法2:#pragma once

关于条件编译,会有专门的详解,在此先简单了解,有个印象即可。


第二步:编译 Compilation

可以通过编译器将*.c / *.i 文件编译生成汇编文件*.s

gcc -S test.c -o test.s/gcc -S test.i -o test.s

查看该汇编文件,在此只展示部分。


第三步:汇编 Assemble

通过*.c / *.i / *.s通过汇编器进行汇编生成二进制文件*.o,生成的二进制文件只需要链接就可以生成可执行文件。

gcc -c test.c -o test.o/gcc -c test.i -o test.o/gcc -c test.s -o test.o

我们可以通过生成的二进制文件制作Linux的动静态库。

第四步:链接 Linking

通过链接器将工程的二进制文件、库函数和启动代码生成可执行文件(名称任意,无要求)。

gcc test.c -o testgcc test.i -o testgcc test.s -o testgcc test.o -o test最后执行即可./test

关于执行命令,是有关shell脚本的知识,在此先了解,之后会出跳转链接!