C 语言机制

IDE 里按下编译键,程序就编程可执行文件了,它应该这么容易吗?

  • 编译、链接

    • .c → 预编译 → .i → 编译 → .s → 汇编 → .o → 链接 → a.out

  • 运行

    • ./a.out

这些背后的原理。gcc 工具,虽然IDE按一个键就行,但是实际上是用了这些东西的。

先来简单实时gcc自己控制编译

/* a.c */
int mian
{
  return 1;
}
$gcc a.c
$./a.out
$echo $?
1

gcc搞除了可执行的文件,运行起来返回了对应的值。

什么是可执行文件呢?直接vi一下a.out就可以看到了。机器看得懂,人看不明白。

:%!xxd

就可以编辑二进制文件了。开头是.ELF这4个字符。

gcc并没啥神秘的,file a.out可以看看a.out这个文件,这是个64bit的二进制文件。

如果不相信这个过程,应该如何去验证这个过程是否正确呢?读gcc的手册man gcc

手册太多了,倒是gcc本身有个跟简短的手册gcc --help,当然也可以使用借助社区维护的命令查询工具tldrarrow-up-right

预编译

这个过程处理C语言里#开头的东西

这两行有什么区别呢?

为什么没安装库会发生错误呢?会提示有个库没找到,那么应该去哪里找呢

现在网络发达,可以先去网上找。那么在命令行的世界呢?

更好的方法是阅读命令的日志

在日志里可以看到,#include <stdio.h>会去系统目录里找,

前面是题外话。

正式来看预编译,这段代码会输出什么呢?

会输出yes。

看预编译指令,所有#的东西都没了,预编译是对代码做的死板的处理。就是复制粘贴。每种程序语言都是字符串,编译器处理都是对字符串的处理。

接下来,为什么aa==bb会成立呢?

这是个语言特性,预编译的指令,不需要定义就可以直接使用了。那么没定义,就是空,这个表达式的意思空 == 空自然是true

所以预处理的宏展开是通过复制/粘贴改变代码形态。

  • #inlcude粘贴文件

  • aabb粘贴符号

知乎问题:如何搞垮一个 OJ?

预处理指令的一些极致应用,

如果true关键在出现在16的倍数行,这时候true就是0。

这些方法如果用的恰当,可以让代码变得很干净,可读性更好。

宏展开就是反复复制粘贴,直到没有宏可以展开为止。

预编译,也称为元编程 (meta-programming)

  • gcc 的预处理器同样可以处理汇编代码

  • C++ 中的模板元编程; Rust 的 macros; ...

宏定义,提供了灵活的用法,接近子安语言的写法。当然也破坏了程序的可读性,程序分析也会变得困难。

编译

首先来看例子,一个不带优化的简易 (理想) 编译器

什么是编译器呢?就是把C语言代码翻译成汇编代码。

C 代码的连续一段总能找到对应的一段连续的机器指令,因此C 是高级的汇编语言!

链接

将多个二进制目标拼接在一起。

不仅仅是两个C文件,甚至C++、rust都可以。

加载

这里就和编译器没啥关系了。./a.out进去了。

静态:C 代码的连续一段总能对应到一段连续的机器指令

动态:C 代码执行的状态总能对应到机器的状态

  • 源代码视角

    • 函数、变量、指针……

  • 机器指令视角

    • 寄存器、内存、地址……

两个视角的共同之处:内存

代码、变量 (源代码视角) = 地址 + 长度 (机器指令视角)

(不太严谨地) 内存 = 代码 + 数据 + 堆栈

因此理解 C 程序执行最重要的就是内存模型

在汇编语言的视角里,所有的东西都是地址

C语言里,一切都可以取地址。可以把任何一个指针赋给void*的指针。

指针首先是个地址。(void*)类型的指针是个纯粹的地址。首先是个纯粹的地址,然后才是指针指向数据的类型。

当指针指向函数的时候,按照64bit类型读,可以直接读出机器码。

再来深入看看C语言里的类型是咋回事。类型是对一段内存的解读方式。C里面所有的数据都可以理解为指针+类型,即地址+对地址的解读

例子:

这不是一段好的代码,只是为了演示一些东西。

这是一个递归调用自己的函数。递归输出所有传入的指令和第一个字符。

argc占用4个字节,argv占用一个地址长度。在C语言函数里,局部变量也在堆栈里,f也是个指针,指向函数。

看起来assert(***a == ch);是一定成立的,是句废话。确实也是废话。但是假设程序是对的,这就是废话,可是假设是错的,我们不知道我写的程序bug啥时候来,就加上了。

最后更新于