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 $?
1gcc搞除了可执行的文件,运行起来返回了对应的值。
什么是可执行文件呢?直接vi一下a.out就可以看到了。机器看得懂,人看不明白。
:%!xxd
就可以编辑二进制文件了。开头是.ELF这4个字符。
gcc并没啥神秘的,file a.out可以看看a.out这个文件,这是个64bit的二进制文件。
如果不相信这个过程,应该如何去验证这个过程是否正确呢?读gcc的手册man gcc
手册太多了,倒是gcc本身有个跟简短的手册gcc --help,当然也可以使用借助社区维护的命令查询工具tldr
预编译
这个过程处理C语言里#开头的东西
这两行有什么区别呢?
为什么没安装库会发生错误呢?会提示有个库没找到,那么应该去哪里找呢
现在网络发达,可以先去网上找。那么在命令行的世界呢?
更好的方法是阅读命令的日志
在日志里可以看到,#include <stdio.h>会去系统目录里找,
前面是题外话。
正式来看预编译,这段代码会输出什么呢?
会输出yes。
看预编译指令,所有#的东西都没了,预编译是对代码做的死板的处理。就是复制粘贴。每种程序语言都是字符串,编译器处理都是对字符串的处理。
接下来,为什么aa==bb会成立呢?
这是个语言特性,预编译的指令,不需要定义就可以直接使用了。那么没定义,就是空,这个表达式的意思空 == 空自然是true
所以预处理的宏展开是通过复制/粘贴改变代码形态。
#inlcude粘贴文件aa,bb粘贴符号
知乎问题:如何搞垮一个 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啥时候来,就加上了。
最后更新于