Linux 纯文本环境C语言极简入门:
环境准备:
- 一个文本编辑器 (VSCode, nano, vi)
- C语言编译器(GCC,Clang都可以)
- 调试器 (GDB, lldb)
以我的环境(Ubuntu 22.04.2 LTS)为例:
build-essential 包是一个在基于 Debian 的 Linux 发行版(如 Ubuntu)中提供的一个元包,包含了
- gcc
- g++
- make
- libc6-dev
- dpkg-dev
sudo apt update
sudo install build-essential nano gdb
开发一个C程序的6个步骤:
- 使用文本编辑器或IDE创建程序源文件
- 预处理
- 编译+汇编
- 链接
- 装载
- 执行
以一个简短的程序为例:
mkdir -p ~/C
cd ~/C
nano Hello_World.c
1. 创建文件
mkdir -p ~/C:递归地创建~/C这个文件夹,如果父目录不存在,则一级一级地创建它;如果父目录存在,则直接创建子目录
cd ~/C:进入子目录
nano Hello_World.c:创建文件Hello_World.c并使用nano编辑器编辑它
Hello_World.c
// Hello_World.c
// This program prints a line of text "Hello World!" then ends.
#include <stdio.h>
int main()
{
printf("Hello World!\n");
return 0;
}
这个程序将在屏幕上打印一行“Hello World!”并换行:
Hello World!
这个程序里涉及到的语法与关键字、函数:
- // 是单行注释,在本行内所有//后的文本(包括//)都会被预处理器删掉
- #include <stdio.h>是预处理指令,它表示在这一行用标准库头文件stdio.h的内容替换这条指令,其中三角括号<header.h>用于包含标准库头文件及系统头文件,
预处理器在标准库目录中搜索#include “myHeader.h” 用于包含用户自定义头文件,首先从源文件所在路径中搜索myHeader.h,如果未找到,就在标准库目录和其他指定目录中寻找,可以使用-I选项增加额外的搜索路径- 用法:gcc -I/path/to/include main.c -o main
- int main()是主函数,每个C程序都从这里开始执行,它由系统调用,它的返回类型是int(整数类型),使用return 0;语句将返回0值(表示程序正常执行)给系统,并在此结束main函数的执行(即return 0;后面的语句都不会被执行)
- printf是C标准库
stdio提供的一个可以向控制台按一定格式输出文本的函数,基本语法为:- printf(格式字符串, 参数1, 参数2,…)
- \n 为换行符,它使得光标在屏幕上移至下一行行首,以反斜杠开始的字符序列为转义字符(Escape Character),对计算机来说,转义符具有字面值以外的特殊含义,如\t是制表符,它使得光标移至下一个制表位,即移动至下一个8字符栏的起点位置。\\表示在字符串中插入一个反斜线(因为单个反斜线表示转义字符的开始),\”表示一个双引号(因为裸双引号会和前面最近的双引号配对,导致结束字符串字面量的表示)
- 格式说明符也可以由字符串字面量构成,如:printf(“Hello World”)
- 按照先后出现的顺序,每一个格式说明符都对应着后面相同序号的参数,这将把对应的参数转换成对应的字符串格式放入格式字符串的相应位置,包括:
| %d | int |
| %ld | long int |
| %lld | long long int |
| %c | char |
| %u | unsigned int |
| %lu | unsigned long int |
| %llu | unsigned long long int |
| %hd | short |
| %hu | unsigned short |
| %s | “字符串” |
| %f | float/double |
| %Lf | long double |
一个函数的结构:
返回类型 函数名(参数列表)
{
函数体
}
其中:
- 返回类型是必须的,诸如int, double等,包括void类型(空类型,表示没有返回值)
- 函数名是标识符,只能以大小写字母或下划线(_)开头,后面为大小写字母、数字或下划线
- 系统函数一般以下划线开头,不建议使用
- 参数列表为空,void,或一个或多个变量声明列表,以逗号分隔,如(返回类型 函数名(参数列表)此三者称为函数头)main函数可写为以下三种形式之一:
- int main(int argc, char **argv)
- argc表示命令行调用此程序时传入参数的个数 + 1, 即包括可执行程序的执行路径;
- argv[0]即包含了调用程序所使用的路径
- int main()
- int main(void)
- int main(int argc, char **argv)
// main_arg.c
#include <stdio.h>
int main(int argc, char **argv)
{
printf("%d\n%s\n", argc, argv[0]);
}
输出
1
/Users/zhaochen/Desktop/C/main_arg
2. 预处理
以Hello_World.c为例,预处理后的结果(Hello_World.i)如下:
开头部分:
# 0 "Hello_World.c"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
# 1 "Hello_World.c"
# 1 "/usr/include/stdio.h" 1 3 4
# 27 "/usr/include/stdio.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/libc-header-start.h" 1 3 4
# 33 "/usr/include/x86_64-linux-gnu/bits/libc-header-start.h" 3 4
# 1 "/usr/include/features.h" 1 3 4
# 392 "/usr/include/features.h" 3 4
# 1 "/usr/include/features-time64.h" 1 3 4
# 20 "/usr/include/features-time64.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4
# 21 "/usr/include/features-time64.h" 2 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/timesize.h" 1 3 4
# 19 "/usr/include/x86_64-linux-gnu/bits/timesize.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4
# 20 "/usr/include/x86_64-linux-gnu/bits/timesize.h" 2 3 4
# 22 "/usr/include/features-time64.h" 2 3 4
# 393 "/usr/include/features.h" 2 3 4
# 486 "/usr/include/features.h" 3 4
结尾部分:
extern int __overflow (FILE *, int);
# 902 "/usr/include/stdio.h" 3 4
# 4 "Hello_World.c" 2
# 5 "Hello_World.c"
int main()
{
printf("Hello World!\n");
return 0;
}
3.编译阶段
在编译阶段,这条指令将预处理过后的程序文本文件转换成为目标平台(特定处理器指令集+操作系统)上的汇编代码文本:
gcc -S Hello_World.i -o Hello_World.s
Hello_World.s:
.file "Hello_World.c"
.text
.section .rodata
.LC0:
.string "Hello World!"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
leaq .LC0(%rip), %rax
movq %rax, %rdi
call puts@PLT
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:
4.汇编阶段
在这个阶段,汇编代码将会转换为特定体系结构+操作系统上能执行的目标(二进制)文件(即机器代码),如:
gcc -c Hello_World.s -o Hello_World.o
或:
as example.s -o example.o
使用objdump 检查目标文件(反汇编全部区段):
objdump -D Hello_World.o
Hello_World.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # f <main+0xf>
f: 48 89 c7 mov %rax,%rdi
12: e8 00 00 00 00 call 17 <main+0x17>
17: b8 00 00 00 00 mov $0x0,%eax
1c: 5d pop %rbp
1d: c3 ret
Disassembly of section .rodata:
0000000000000000 <.rodata>:
0: 48 rex.W
1: 65 6c gs insb (%dx),%es:(%rdi)
3: 6c insb (%dx),%es:(%rdi)
4: 6f outsl %ds:(%rsi),(%dx)
5: 20 57 6f and %dl,0x6f(%rdi)
8: 72 6c jb 76 <main+0x76>
a: 64 21 00 and %eax,%fs:(%rax)
Disassembly of section .comment:
0000000000000000 <.comment>:
0: 00 47 43 add %al,0x43(%rdi)
3: 43 3a 20 rex.XB cmp (%r8),%spl
6: 28 55 62 sub %dl,0x62(%rbp)
9: 75 6e jne 79 <main+0x79>
b: 74 75 je 82 <main+0x82>
d: 20 31 and %dh,(%rcx)
f: 31 2e xor %ebp,(%rsi)
11: 34 2e xor $0x2e,%al
13: 30 2d 31 75 62 75 xor %ch,0x75627531(%rip) # 7562754a <main+0x7562754a>
19: 6e outsb %ds:(%rsi),(%dx)
1a: 74 75 je 91 <main+0x91>
1c: 31 7e 32 xor %edi,0x32(%rsi)
1f: 32 2e xor (%rsi),%ch
21: 30 34 29 xor %dh,(%rcx,%rbp,1)
24: 20 31 and %dh,(%rcx)
26: 31 2e xor %ebp,(%rsi)
28: 34 2e xor $0x2e,%al
2a: 30 00 xor %al,(%rax)
Disassembly of section .note.gnu.property:
0000000000000000 <.note.gnu.property>:
0: 04 00 add $0x0,%al
2: 00 00 add %al,(%rax)
4: 10 00 adc %al,(%rax)
6: 00 00 add %al,(%rax)
8: 05 00 00 00 47 add $0x47000000,%eax
d: 4e 55 rex.WRX push %rbp
f: 00 02 add %al,(%rdx)
11: 00 00 add %al,(%rax)
13: c0 04 00 00 rolb $0x0,(%rax,%rax,1)
17: 00 03 add %al,(%rbx)
19: 00 00 add %al,(%rax)
1b: 00 00 add %al,(%rax)
1d: 00 00 add %al,(%rax)
...
Disassembly of section .eh_frame:
0000000000000000 <.eh_frame>:
0: 14 00 adc $0x0,%al
2: 00 00 add %al,(%rax)
4: 00 00 add %al,(%rax)
6: 00 00 add %al,(%rax)
8: 01 7a 52 add %edi,0x52(%rdx)
b: 00 01 add %al,(%rcx)
d: 78 10 js 1f <.eh_frame+0x1f>
f: 01 1b add %ebx,(%rbx)
11: 0c 07 or $0x7,%al
13: 08 90 01 00 00 1c or %dl,0x1c000001(%rax)
19: 00 00 add %al,(%rax)
1b: 00 1c 00 add %bl,(%rax,%rax,1)
1e: 00 00 add %al,(%rax)
20: 00 00 add %al,(%rax)
22: 00 00 add %al,(%rax)
24: 1e (bad)
25: 00 00 add %al,(%rax)
27: 00 00 add %al,(%rax)
29: 45 0e rex.RB (bad)
2b: 10 86 02 43 0d 06 adc %al,0x60d4302(%rsi)
31: 55 push %rbp
32: 0c 07 or $0x7,%al
34: 08 00 or %al,(%rax)
这个阶段的产物不能够在操作系统上直接执行,仍需要与标准库、系统库,系统启动代码与自定义库文件进行链接。
5.链接阶段
要想生成最终可在操作系统上执行的程序,必须将生成的目标文件与特定的系统库文件(静态库、动态库)链接,
使用如下命令链接:
gcc main.o -L/path/to/lib -lmylibrary -o myprogram