CTF-All-In-One/doc/1.5.1_c_basic.md
firmianay bb403d3303 fix
2017-08-16 21:49:30 +08:00

5.5 KiB
Raw Blame History

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.hlimits.h 中:

$ man stdint.h
$ cat /usr/include/stdint.h
$ man limits.h
$ cat /usr/include/limits.h

了解整数的符号和大小是很有用的,在后面的相关章节中我们会介绍整数溢出的内容。