编写C程序,是一个从源码到可执行文件的过程,该过程需要编译器提供帮助。
通过编译器,将C的源代码转换成为二进制可执行文件。二进制可执行文件,顾名思义,二进制,也就是CPU(或者说当前计算机体系结构)能够识别的指令集合,可运行,该文件不仅是指令的集合,同时已经和当前计算机环境做了链接,它知道访问当前操作系统的相关能力的入口(或接口)在哪里。
这点同Java的区别是很大的,Java编译出了class文件,而这个文件可以在任何装有JVM虚拟机的机器上运行,但是注意,运行的方式是:java T
。这里运行的其实不是编写的T.class,而是java
这个程序,依靠java
程序来解释运行的T.class。
通过编译器编译的C程序,是可以直接运行的,其实编译出来的产物等同于这台机器上的java
程序。
假设存在一个编译好的T.class文件。
存在下面的C程序,我们需要对它进行编译。
#include <stdio.h>
#define X 10
int main(void) {
int i = X;
printf("i的值是:%d\n", i);
return 0;
}
在Mac上,C的编译器底层实际是LLVM,如果使用gcc,也是可以,只不过它是通过llvm-gcc桥接到了LLVM。
安装完XCode后,就具备了编译C程序的能力,当然不同的系统平台需要找到各自对应的编译器。
基于LLVM的clang的编译产出相比gcc更好,同时受到版权的限制也少,目前大部分软件,包括:Chrome都选择使用clang进行编译构建。
运行命令。
% cc -o test test.c
将test.c
编译成可执行文件test
,然后选择运行。
% ./test
i的值是:10
可以看到,这个test
程序和java
一样,都是二进制可执行文件,但是把它拷贝到windows机器上,就不能双击运行了,因为它包含的指令不能够被windows机器识别,同时它是和当前机器做了链接,没有同windows机器做链接。
这里说了这么多指令和链接,那么编译一个C程序需要经过哪些步骤呢?
可以看到需要经历主要的三个步骤:预处理、编译和链接。如果使用过脚本语言(或者模板引擎)的,对于预处理肯定不会陌生,它基本就是对源文件做包含和转换等操作。
C程序进行预处理都是对指令进行操作,比如:#include
或者#define
指令,它们都以#
开头,预处理器就关注这些内容。
预处理器对#include <stdio.h>
指令的处理,就是在编译器中找到源文件中需要的头文件stdio.h
,将其包含进来。
预处理器对#define X 10
指令的处理,就是将源文件中X
,替换为10
。
接下来编译器会将处理过的源文件进行编译,生成二进制目标代码,最终通过链接器将系统文件同目标代码进行整合链接,完成本地化,使之能够运行。
Java就不是这个套路,Java编译器只是将源码编译为class文件,而并没有链接。这个过程有些类似将源码编译成为一种中间状态的文件,然后靠各个平台的程序(完成了本地化)来解释运行这个文件。
就像一个html文件,不同平台的浏览器来解释运行它一样,这个过程就需要类似JVM的程序托着这个文件。
可以看出来C程序的编译步骤要比Java这种托管的程序来的复杂,我们可以慢动作的看一下这个过程。
运行cc -E test.c > test.i
,输出预处理器处理后的文件,可以看到该文件(部分内容):
# 1 "test.c"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 368 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "test.c" 2
# 1 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/stdio.h" 1 3 4
# 64 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/stdio.h" 3 4
# 1 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_stdio.h" 1 3 4
# 68 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_stdio.h" 3 4
typedef union {
char __mbstate8[128];
long long _mbstateL;
} __mbstate_t;
typedef __mbstate_t __darwin_mbstate_t;
typedef long int __darwin_ptrdiff_t;
FILE *fopen(const char * restrict __filename, const char * restrict __mode) __asm("_" "fopen" );
int fprintf(FILE * restrict, const char * restrict, ...) __attribute__((__format__ (__printf__, 2, 3)));
int getc(FILE *);
int getchar(void);
char *gets(char *);
void perror(const char *) __attribute__((__cold__));
int printf(const char * restrict, ...) __attribute__((__format__ (__printf__, 1, 2)));
int main(void) {
int i = 10;
printf("i的值是:%d\n", i);
return 0;
}
在文件中通过#include
指令包含的头文件,内容被包含进了该文件,同时#define
定义的内容已经做了替换。
#define X 10
,其中X
已经替换成了10
。
接下来运行cc -S test.i > test.s
,将预处理器处理完成的文件作为输入,输出汇编文件。
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 11, 0 sdk_version 11, 3
.globl _main ## -- Begin function main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $16, %rsp
movl $0, -4(%rbp)
movl $10, -8(%rbp)
movl -8(%rbp), %esi
leaq L_.str(%rip), %rdi
movb $0, %al
callq _printf
xorl %eax, %eax
addq $16, %rsp
popq %rbp
retq
.cfi_endproc
## -- End function
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "i\347\232\204\345\200\274\346\230\257\357\274\232%d\n"
.subsections_via_symbols
可以看到汇编指令描述的程序,这里不做展开。然后运行cc -c test.s > test.o
,将汇编文件编译为目标二进制文件。
<CF><FA><ED><FE>^G^@^@^A^C^@^@^@^A^@^@^@^D^@^@^@^H^B^@^@^@ ^@^@^@^@^@^@^Y^@^@^@<88>^A^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@<A0>^@^@^@^@^@^@^@(^B^@^@^@^@^@^@<A0>^@^@^@^@^@^@^@^G^@^@^@^G^@^@^@^D^@^@^@^@^@^@^@__text^@^@^@^@^@^@^@^@^@^@__TEXT^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@/^@^@^@^@^@^@^@(^B^@^@^D^@^@^@<C8>^B^@^@^B^@^@^@^@^D^@<80>^@^@^@^@^@^@^@^@^@^@^@^@__cstring^@^@^@^@^@^@^@__TEXT^@^@^@^@
^@^@^@^@^@^@/^@^@^@^@^@^@^@^Q^@^@^@^@^@^@^@W^B^@^@^@^@^@^@^@^@^@^@^@^@^@^@^B^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@__compact_unwind__LD^@^@^@^@^@^@^@^@^@^@^@^@@^@^@^@^@^@^@^@ ^@^@^@^@^@^@^@h^B^@^@^C^@^@^@<D8>^B^@
^@^A^@^@^@^@^@^@^B^@^@^@^@^@^@^@^@^@^@^@^@__eh_frame^@^@^@^@^@^@__TEXT^@^@^@^@^@^@^@^@^@^@`^@^@^@^@^@^@^@@^@^@^@^@^@^@^@<88>^B^@^@^C^@^@^@^@^@^@^@^@^@^@^@^K^@^@h^@^@^@^@^@^@^@^@^@^@^@^@2^@^@^@^X^@^@^@^A^@^@^@^@^@^K^@^@^C^K^@^@^@^@^@^B^@^@^@^X^@^@^@<E0>^B^@^@^B^@^@^@^@^C^@^@^P^@^@^@^K^@^@^@P^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^A^@^@^@^A^@^@^@^A^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@
^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@UH<89><E5>H<83><EC>^P<C7>E<FC>^@^@^@^@<C7>E<F8>
^@^@^@<8B>u<F8>H<8D>=^O^@^@^@<B0>^@<E8>^@^@^@^@1<C0>H<83><C4>^P]<C3>i的值是:%d
^@^@^@^@^@^@^@^@^@/^@^@^@^@^@^@^A^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^T^@^@^@^@^@^@^@^AzR^@^Ax^P^A^P^L^G^H<90>^A^@^@$^@^@^@^\^@^@^@<80><FF><FF><FF><FF><FF><FF><FF>/^@^@^@^@^@^@^@^@A^N^P<86>^BC^M^F^@^@^@^@^@^@^@#^@^@^@^A^@^@-^\^@^@^@^B^@^@^U^@^@^@^@^A^@^@^F^A^@^@^@^O^A^@^@^@^@^@^@^@^@^@^@^G^@^@^@^A^@^@^@^@^@^@^@^@^@^@^@^@_main^@_printf^@^@
test.o (END)
这里面内容就是机器指令了,人类已经无法阅读,但是它还是不能执行,需要同当前系统环境进行链接。
运行cc test.o -o test
,将目标二进制文件进行链接,生成可执行文件test
。
% ./test
i的值是:10
C程序实际就是一堆函数的集合,其程序代码可以看做有以下组成。
C程序就是编写函数,开发者写的是应用函数,而编译器提供的是本系统环境的系统函数,或者叫标准库,它们有能力同系统进行交互,处于程序调用的底层。
看一个C程序。
指令、函数和语句构成了C程序。
变量或者常量的命名同Java的规范一样,这些命名的变量和常量可以被称为标识符。
C程序是函数的集合,同时如果以符号来看,实际也是一堆记号(符号)的集合。
从形式上看,一个C程序就是定义一些标识符(变量、常量(或宏)和函数),中间使用语言的关键字来定义数据类型,依靠字面量以及运算符来进行运算,同时需要标点符号来分割不同的记号。由此可见,C程序是简单的,这种简单是由其语言本身的简单所带来的优点,它扩充能力的方式是通过调用不同的函数库,而不是增加语言的特性(包括语法特性等)和功能。