5.6 KiB
C 语言基础
从源代码到可执行文件
我们以经典著作《The C Programming Language》中的第一个程序 “Hello World” 为例,讲解 Linux 下 GCC 的编译过程。
#include <stdio.h>
main()
{
printf("hello, world\n");
}
$gcc hello.c
$./a.out
hello world
以上过程可分为4个步骤:预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)和链接(Linking)。
预编译
$gcc -E hello.c -o hello.i
预编译过程主要处理源代码中以 “#” 开始的预编译指令:
- 将所有的 “#define” 删除,并且展开所有的宏定义。
- 处理所有条件预编译指令,如 “#if”、“#ifdef”、“#elif”、“#else”、“#endif”。
- 处理 “#include” 预编译指令,将被包含的文件插入到该预编译指令的位置。注意,该过程递归执行。
- 删除所有注释。
- 添加行号和文件名标号。
- 保留所有的 #pragma 编译器指令。
编译
$gcc -S hello.c -o hello.s
编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生成相应的汇编代码文件。
汇编
$gcc -c hello.s -o hello.o
或者
$gcc -c hello.c -o hello.o
汇编器将汇编代码转变成机器可以执行的指令。
链接
目标文件需要链接一大堆文件才能得到最终的可执行文件。链接过程主要包括地址和空间分配(Address and Storage Allocation)、符号决议(Symbol Resolution)和重定向(Relocation)等。
C 语言标准库
C 运行库(CRT)是一套庞大的代码库,以支撑程序能够正常地运行。其中 C 语言标准库占据了最主要地位。
常用的标准库文件头:
- 标准输入输出(stdio.h)
- 字符操作(ctype.h)
- 字符串操作(string.h)
- 数学函数(math.h)
- 实用程序库(stdlib.h)
- 时间/日期(time.h)
- 断言(assert.h)
- 各种类型上的常数(limits.h & float.h)
- 变长参数(stdarg.h)
- 非局部跳转(setjmp.h)
glibc 即 GNU C Library,是为 GNU 操作系统开发的一个 C 标准库。glibc 主要由两部分组成,一部分是头文件,位于 /usr/include
;另一部分是库的二进制文件。二进制文件部分主要是 C 语言标准库,有动态和静态两个版本,动态版本位于 /lib/libc.so.6
,静态版本位于 /usr/lib/libc.a
。
整数表示
默认情况下,C 语言中的数字是有符号数,下面我们声明一个有符号整数和无符号整数:
int var1 = 0;
unsigned int var2 = 0;
- 有符号整数
- 可以表示为正数或负数
int
的范围:-2,147,483,648 ~ 2,147,483,647
- 无符号整数
- 只能表示为零或正数
unsigned int
的范围:0 ~ 4,294,967,295
signed
或者 unsigned
取决于整数类型是否可以携带标志 +/-
:
- Signed
- int
- signed int
- long
- Unsigned
- unit
- unsigned int
- unsigned long
在 signed int
中,二进制最高位被称作符号位,符号位被设置为 1
时,表示值为负,当设置为 0
时,值为非负:
- 0x7FFFFFFF = 2147493647
- 01111111111111111111111111111111
- 0x80000000 = -2147483647
- 10000000000000000000000000000000
- 0xFFFFFFFF = -1
- 11111111111111111111111111111111
二进制补码以一种适合于二进制加法器的方式来表示负数,当一个二进制补码形式表示的负数和与它的绝对值相等的正数相加时,结果为 0。首先以二进制方式写出正数,然后对所有位取反,最后加 1 就可以得到该数的二进制补码:
eg: 0x00123456
= 1193046
= 00000000000100100011010001010110
~= 11111111111011011100101110101001
+= 11111111111011011100101110101010
= -1193046 (0xFFEDCBAA)
编译器需要根据变量类型信息编译成相应的指令:
- 有符号指令
- IDIV:带符号除法指令
- IMUL:带符号乘法指令
- SAL:算术左移指令(保留符号)
- SAR:右移右移指令(保留符号)
- MOVSX:带符号扩展传送指令
- JL:当小于时跳转指令
- JLE:当小于或等于时跳转指令
- JG:当大于时跳转指令
- JGE:当大于或等于时跳转指令
- 无符号指令
- DIV:除法指令
- MUL:乘法指令
- SHL:逻辑左移指令
- SHR:逻辑右移指令
- MOVZX:无符号扩展传送指令
- JB:当小于时跳转指令
- JBE:当小于或等于时跳转指令
- JA:当大于时跳转指令
- JAE:当大于或等于时跳转指令
32 位机器上的整型数据类型,不同的系统可能会有不同:
C 数据类型 | 最小值 | 最大值 | 最小大小 |
---|---|---|---|
char | -128 | 127 | 8 bits |
short | -32 768 | 32 767 | 16 bits |
int | -2 147 483 648 | 2 147 483 647 | 16 bits |
long | -2 147 483 648 | 2 147 483 647 | 32 bits |
long long | -9 223 372 036 854 775 808 | 9 223 372 036 854 775 807 | 64 bits |
固定大小的数据类型:
-
int [# of bits]_t
- int8_t, int16_t, int32_t
-
uint[# of bits]_t
- uint8_t, uint16_t, uint32_t
-
有符号整数
-
无符号整数
更多信息在 stdint.h
和 limits.h
中:
$ man stdint.h
$ cat /usr/include/stdint.h
$ man limits.h
$ cat /usr/include/limits.h
了解整数的符号和大小是很有用的,在后面的相关章节中我们会介绍整数溢出的内容。