《跟着符映维学c语言--配置c语言开发环境》
今天,我们来学习基于debian配置c语言开发环境
先写出我们今天要讲解的内容大纲
功能需求:配置c语言开发环境和学会如何编译
- 系统环境,debian
- 终端工具,tmux(可选)
- 编译器,gcc
- 文本编辑工具,vim
- 简单的hello.c代码
- 使用gcc编译hello.c为可执行文件
- 在debian中运行可执行文件
- 了解gcc的编译过程
- 使用gcc进行分阶段编译
- gcc的常见编译选项
- 多文件编译
- 静态库创建与使用
- 动态库创建与使用
- 调试编译
- 交叉编译
- 依赖检查
- 常见的问题排查
然后我们对以上的大纲进行模块化
一、系统环境,debian
- 安装debian
- 我们打开
https://www.debian.org/distrib/
官网相关下载路径,
选择下载64 位 PC DVD-1 iso
- 下载完成后,我们就可以把iso写入u盘,制作启动盘安装debian
- 具体安装步骤我这里就不一一讲解,具体的详细安装过程请自行查询
- 我们打开
二、终端工具,tmux(可选)
-
安装tmux
sudo apt update && sudo apt install tmux
-
验证安装
tmux -V
-
安装完成后我们创建会话
tmux new -s fyw
三、编译器,gcc
- 安装gcc
安装gcc,我们有两种安装选择.- (1)安装标准库和相关的工具
sudo apt update && sudo apt install build-essential
- (2)只安装基础标准c库
sudo apt update && sudo apt install libc6-dev
注意:
如果安装了build-essential
就不需要再安装libc6-dev
因为build-essential
已经包含了libc6-dev
- 验证安装
gcc --version
四、文本编辑工具,vim
- 安装vim
一般在我们安装好debian的时候,vim是debian自带的,可以直接使用.
sudo update && sudo apt install vim
如果想需要更多的功能支持,可以
sudo update && sudo apt install vim-nox
vim-nox 是vim的一个无gui的功能增强版本
- 验证安装
vim --version
五、简单的hello.c代码
#include <stdio.h>
int main(void){printf("hello,world!");return 0;
}
六、使用gcc编译hello.c为可执行文件
gcc hello.c -o hello
七、在debian中运行可执行文件
./hello
八、了解gcc的编译过程
gcc的编译过程有4个阶段
- 预处理阶段
- 编译阶段
- 汇编阶段
- 链接阶段
九、使用gcc进行分阶段编译
- 预处理阶段
gcc -E hello.c -o hello.i
- 编译阶段
gcc -S hello.i -o hello.s
- 汇编阶段
gcc -c hello.s -o hello.o
- 链接阶段
gcc hello.o -o hello
以上的分阶段编译后,我们就中以通过以下命令运行可执行文件
./hello
十、gcc的常见编译选项
-Wall
: 启用所有警告信息Wextra
: 启用额外警告-Werror
: 将警告视为错误-g
: 生成调试信息-o
: 指定输出文件名-O0 -O1 -O2 -O3 -Os
: 指定优化级别-E
:只运行预处理器-S
:生成汇编代码-c
:只编译不链接-lm
: 链接数学库-fsanitize=address
: 检测内存错误
现在我们通过实际操作直观的去理解以上常见的编译选项.
(1)-Wall
我们写一个main.c
代码
#include <stdio.h>
int add(int a,int b){return a+b;
}
int main(void){int a=5,b=3;int result=add(a,b);if(result=8){ //正确应该是result==8printf("ok!\n");}else{printf("no!\n");}return 0;
}
以上是一个存在错误的c语言代码.我们进行编译
gcc main.c -o main
./main
我们发现,gcc正常编译生成可执行文件
输出
ok!
这是我们不想看到的,因为main.c
是一个存在错误的代码.
我们希望的是gcc进行编译的时候,能捕获相关的错误代码.这时候,我们可以使用-Wall
选项
gcc -Wall main.c -o main
编译后,返回
main.c: In function ‘main’:
main.c:8:12: warning: suggest parentheses around\assignment used as truth value [-Wparentheses]8 | if(result=8){| ^~~~~~
我们看到,gcc捕获了到了相关的错误代码.并提示我们.
(2)-Wextra
我们把以上的main.c
代码改为
#include <stdio.h>
int main(void){int x=5;unsigned int y=3;if(x<y){ //有符号和无符号进行比较printf("x<y\n");}else{printf("x>y\n");}return 0;
}
编译
gcc -Wall main.c -o main
我们发现,gcc正常编译并生成了可执行文件.无法捕获有符号和无符号进行比较.
现在我们添加-Wextra
选项进行编译
gcc -Wall -Wextra main.c -o main
返回
main.c: In function ‘main’:
main.c:5:13: warning: comparison of integer expressions\of different signedness: ‘int’ and ‘unsigned int’ [-Wsign-compare]5 | if(x<y){| ^
gcc捕获到了有符号和无符号比较的问题.
(3)-Werror
我们把以上的main.c
代码改为.
#include <stdio.h>
int main(void){int c=10;int a=5,b=3;int result=a+b;printf("a+b=%d\n",result);return 0;
}
编译
gcc -Wall -Wextra main.c -o main
返回
main.c: In function ‘main’:
main.c:3:13: warning: unused variable ‘c’ [-Wunused-variable]3 | int c=10;| ^
gcc提示了一个警告warning
,但不是错误.我们添加选项-Werror
进行编译
gcc -Wall -Wextra -Werror main.c -o main
返回
main.c: In function ‘main’:
main.c:3:13: error: unused variable ‘c’ [-Werror=unused-variable]3 | int c=10;| ^
cc1: all warnings being treated as errors
我们看到,所有警告都被视为了错误error
(4)-g
我们把main.c
改为
#include <stdio.h>
int main(void){printf("hello!\n");return 0;
}
编译
gcc -Wall -Wextra -Werror main.c -o main
我们使用file
查看main
,返回
main: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV)\
, dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2\
, BuildID[sha1]=3d29aa44da638e2807fe7887acc1427248572d35\
, for GNU/Linux 3.2.0, not stripped
我们看到,生成的可执行文件main
并没有包含调试信息,我们添加-g
进行编译
gcc -Wall -Wextra -Werror -g main.c -o main
我们再使用file
查看main
,返回
main: ELF 64-bit LSB pie executable, x86-64,\version 1 (SYSV), dynamically linked, interpreter\/lib64/ld-linux-x86-64.so.2, BuildID\
[sha1]=eced594d38356600bdd56b0015cc601f47af61bd,\for GNU/Linux 3.2.0, with debug_info, not stripped
我们看到,这次的返回信息中包含了with debug_info
,说明已经生成了调试信息.
(5)-o
-o
为指定输出文件名,如: gcc main.c -o main
,main
就为输出的可执行文件名.
(6)-O0 -O1 -O2 -O3 -Os
-O0 -O1 -O2 -O3 -Os
为优化级别选项,这里我们通过main.c
通过不同的优化级别,
生成汇编代码,然后进行比较.来直观的去看他们的优化级别.
我们把main.c
改为
#include <stdio.h>
int main(void){printf("hello\n");return 0;
}
然后编译
gcc -S -O0 main.c -o mainO0.s
gcc -S -O1 main.c -o mainO1.s
gcc -S -O2 main.c -o mainO2.s
gcc -S -O3 main.c -o mainO3.s
gcc -S -Os main.c -o mainOs.s
我们使用vim进行比较
vim -d mainO0.s mainO1.s
在vim中,会高亮显示两个文件差异的地方.具体的汇编区别,这里不进行详细解释.
这里我们需要明白的是,不同的优化级别,所执行的编译方式会有所不同.
(7)(8)(9)-E -S -c
-E -S -c
分别生成预处理.i
文件,编译.s
文件,和汇编.o
文件
我们进行编译查看区别
gcc -E main.c
gcc -S main.c
gcc -c main.c
我们看到,不同的选项生成了相应的文件.
(10)-lm
我们把以上main.c
改为
#include <stdio.h>
#include <math.h>
int main(void){int a=6;int result=sqrt(a);printf("sqrt(a)=%d\n",result);return 0;
}
编译
gcc main.c -o main
返回
/usr/bin/ld: /tmp/ccByXknH.o: in function `main':
main.c:(.text+0x23): undefined reference to `sqrt'
collect2: error: ld returned 1 exit status
我们看到提示了错误,这个错误是因为我们没有链接数学库造成的.我们添加-lm
选项再次编译
gcc main.c -o main -lm
我们看到,gcc成功编译为可执行文件.
(11)-fsanitize=address
我们把以上main.c
改为
#include <stdio.h>
int main(void){int arr[5]={1,2,3,4,5};arr[5]=6; //越界printf("%d\n",arr[5]);return 0;
}
编译
gcc main.c -o main
我们看到gcc编译成功,我们运行可执行文件 ./main
,返回
6
这显示是错误的,而且这种错误我们不容易发现.但gcc并没有捕获到这个边界问题.
我们添加编译选项-fsanitize=address
再次编译
gcc -fsanitize=address main.c -o main
我们看到gcc编译成功,我们运行可执行文件 ./main
返回..
==109849==ERROR: AddressSanitizer:\stack-buffer-overflow on address 0x7fff9451dc34 at pc 0x561
48dfcf389 bp 0x7fff9451dbf0 sp 0x7fff9451dbe8
WRITE of size 4 at 0x7fff9451dc34 thread T0 #0 0x56148dfcf388 in main\(/home/fuyingwei/document/main/test/testdddd/main+0x1388) #1 0x7fe1e9e46249 in\__libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58 #2 0x7fe1e9e46304 in __libc_start_main_impl ../csu/libc-start.c:360 #3 0x56148dfcf0d0 in _start\(/home/fuyingwei/document/main/test/testdddd/main+0x10d0) Address 0x7fff9451dc34 is located in stack of thread T0 at offset 52 in frame #0 0x56148dfcf1a8 in main (/home/fuyingwei/document/main/test/testdddd/main+0x11a8) This frame has 1 object(s): [32, 52) 'arr' (line 3) <== Memory access at offset 52 overflows this variable
HINT: this may be a false positive\if your program uses some custom stack unwind mechanism, swapcontext or vfork(longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer:\stack-buffer-overflow\(/home/fuyingwei/document/main/test/testdddd/main+0x1388) in main
Shadow bytes around the buggy address:0x10007289bb30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x10007289bb40: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x10007289bb50: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x10007289bb60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x10007289bb70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x10007289bb80: f1 f1 f1 f1 00 00[04]f3 f3 f3 f3 f3 00 00 00 000x10007289bb90: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x10007289bba0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x10007289bbb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x10007289bbc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x10007289bbd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):Addressable: 00Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: faFreed heap region: fdStack left redzone: f1Stack mid redzone: f2Stack right redzone: f3Stack after return: f5Stack use after scope: f8Global redzone: f9Global init order: f6Poisoned by user: f7Container overflow: fcArray cookie: acIntra object redzone: bbASan internal: feLeft alloca redzone: caRight alloca redzone: cb
==109849==ABORTING
我们看到ASan捕获到了这个边界问题,并提示了我们.
十一、多文件编译
我们创建以下的目录文件结构
project/|---main.c|---utils.h|---utils.c
utils.h
#ifndef UTILS_H
#define UTILS_H
int add(int a,int b);
#endif
utils.c
#include "utils.h"
int add(int a,int b){return a+b;
}
main.c
#include <stdio.h>
#include "utils.h"
int main(void){int a=5,b=3;int result=add(a,b);printf("a+b=%d\n",result);return 0;
}
多文件编译的方法
方法一:分开编译
gcc main.c
gcc utils.c
gcc main.o utils.o hello
方法二:直接编译
gcc main.c utils.c -o hello
十二、静态库创建与使用
- 静态库创建
gcc -c utils.c -o utils.o
ar rcs libutils.a utils.o
gcc main.c -L. -lutils -o static_hello
- 运行程序
./hello
提示: 静态库所创建的可执行文件,不会受库的影响,也就是说
当我们使用静态库创建可执行文件后,对静态库进行改名或删除,可执行文件都能运行
十三、动态库创建与使用
- 动态库创建与使用
gcc -c -fPIC utils.c -o utils.o
gcc -shared -o libutils.so utils.o
gcc main.c -L. -lutils -o dynamic_hello
- 运行程序
expore LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
./hello
注意: 动态库所创建的可执行文件,受动态库的影响,如果修改动态库的名称或删除,
都会导致可执行文件无法运行.
十四、调试编译
在gcc中,调试编译的选项是-g
,我们在检查一个可执行文件的时候,我们可以通过
file
检查是否包含调试信息
file 可执行文件
如果返回的信息中包含debug_info
那么说明可执行文件包含可调试信息.
如果没有,我们可以
gcc -g main.c -o main
包含调试信息后,我们就可以通过gdb进行调试.关于gdb的调试,后面会说.这里先不赘述.
十五、交叉编译
交叉编译是指在当前平台中,生成目标平台(另一个平台)可运行的代码文件.
比如我们要生成在ARM64架构的程序.
- 安装适合我们系统环境的相关编译包
sudo apt update && sudo apt aarch64-linux-gnu-gcc
- 编译目标平台的可运行代码文件
aarch64-linux-gnu-gcc main.c -o main
我们用file
查看,会看到返回包含/lib/ld-linux-aarch64.so.1
的信息.
十六、依赖检查
- 基本依赖检查
我们可以这样
gcc -M main.c
- 排除系统头文件的依赖
gcc -MM main.c
- 动态依赖检查
当我们用file
查看可执行文件,如果包含类似/lib64/ld-linux-x86-64.so.2
的信息,
我们就可以
ldd 可执行文件
查看相关的动态依赖,返回类似的信息
linux-vdso.so.1 (0x00007fff32992000)libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe50829e000)/lib64/ld-linux-x86-64.so.2 (0x00007fe50849e000)
- 预处理依赖检查
我们可以使用
gcc -H main.c
查看预处理相关的依赖文件路径
十七、gcc常见问题排查
1. 编译错误类问题
(1)"No such file or directory" 错误
- 问题:找不到头文件或源文件
- 可能原因:
- 头文件路径未正确设置
- 文件名拼写错误
- 文件确实不存在
- 错误:
fatal error: xxx.h: No such file or directory
- 解决方案:
# 使用-I选项指定头文件路径 gcc -I/path/to/include -o output source.c
或
sudo apt update && sudo apt install build-essential
(2)未定义的引用错误 (Undefined reference)
- 问题:链接时找不到函数或变量定义
- 可能原因:
- 未链接必要的库
- 函数/变量名拼写错误
- 库文件顺序不正确
- 错误:
undefined reference to 'function_name'
- 解决方案:
# 使用-l选项链接库,注意库的顺序 gcc -o program source.c -lm -lxyz
(3)版本不兼容错误
- 问题:代码使用新特性但编译器版本过旧
- 错误:
error: 'for' loop initial declarations are only allowed in C99 mode note: use option -std=c99 or -std=gnu99 to compile your code
- 解决方案:
# 检查GCC版本 gcc --version# 使用特定标准编译 gcc -std=c11 -o output source.c
2. 警告类问题
(1)隐式声明警告 (Implicit declaration)
- 问题:函数未声明就使用
- 警告:
warning: implicit declaration of function
'func_name' [-Wimplicit-function-declaration]
- **解决方案**:
- 包含正确的头文件
- 在使用前声明函数原型**(2)未使用变量警告 (Unused variable)**
- **问题**:定义了但未使用的变量
- **警告**:
warning: unused variable 'var_name' [-Wunused-variable]
- **解决方案**:
- 删除无用变量
- 使用 `(void)variable;` 显式忽略
- 或添加 `-Wno-unused-variable` 关闭此警告
```bash
gcc -Wno-unused-variable -o program source.c
3. 链接问题
(1)库路径问题
- 问题:找不到库文件
- 错误:
/usr/bin/ld: cannot find -lxyz
- 解决方案:
# 使用-L指定库路径 gcc -L/path/to/libs -o program source.c -lxyz
(2)静态库与动态库冲突
- 问题:同时存在同名的静态库(.a)和动态库(.so)
- 错误:
/usr/bin/ld: warning: libxyz.so, needed by ..., may conflict with libxyz.a
- 解决方案:
- 明确指定要链接的库类型
- 使用
-static
强制静态链接
4. 运行时问题
(1)动态链接库找不到
- 问题:运行时找不到.so文件
- 错误:
error while loading shared libraries: libxyz.so:
cannot open shared object file: No such file or directory
- **解决方案**:
```bash
# 设置LD_LIBRARY_PATH环境变量
export LD_LIBRARY_PATH=/path/to/libs:$LD_LIBRARY_PATH# 或者使用-rpath选项
gcc -Wl,-rpath,/path/to/libs -o program source.c -lxyz
(1)段错误 (Segmentation fault)
- 问题:访问非法内存
- 错误:
Segmentation fault (core dumped)
- 解决方案:
- 使用
-g
编译并调试
gcc -g -o program source.c gdb ./program
- 使用地址检查工具如Valgrind
- 使用
5. 优化问题
(1)优化导致程序行为异常
- 问题:使用-O2/-O3优化后程序出错
- 错误:
Program behaves differently with -O2/-O3 optimization
- 解决方案:
- 检查代码中是否有未定义行为
- 逐步增加优化级别定位问题
- 使用
-fno-strict-aliasing
等选项关闭特定优化
6. 其他常见错误
(1)语法错误 (Syntax error)
- 错误:
error: expected ';' before '}' token
- 问题:缺少分号、括号不匹配等基本语法错误
- 解决方案:
- 检查错误指示行附近是否缺少分号
- 检查所有括号是否成对出现
- 使用代码编辑器的括号匹配功能辅助检查
- 示例修正:
// 错误示例 int a = 1 // 修正后 int a = 1;
(2)类型不匹配 (Type mismatch)
- 错误:
error: incompatible types when assigning to type 'int' from type 'float'
- 问题:变量类型不兼容的赋值或操作
- 解决方案:
- 检查变量类型声明
- 必要时进行显式类型转换
- 示例修正:
// 错误示例 int a = 3.14; // 修正后 int a = (int)3.14;
**(3)多重定义 (Multiple definition)
- 错误:
/usr/bin/ld: main.o:/path/to/file.h:23:
multiple definition of 'global_var'; first defined here
- **问题**:全局变量在多个文件中定义
- **解决方案**:
1. 在头文件中使用 `extern` 声明变量
2. 在单个源文件中定义变量
3. 示例修正:```c// file.hextern int global_var; // 声明// file.cint global_var = 0; // 定义```**(4)重定义 (Redefinition)**
- **原错误**:
error: redefinition of 'struct_name'
- **问题**:结构体/类型重复定义
- **解决方案**:
1. 使用头文件保护宏
2. 检查是否在不同地方重复定义相同结构体
3. 示例修正:```c// file.h#ifndef FILE_H#define FILE_Hstruct MyStruct {int a;};#endif```**(5)缺少返回语句 (Missing return)**
- **原错误**:
warning: control reaches end of non-void function [-Wreturn-type]
- **问题**:非void函数可能没有返回值
- **解决方案**:
1. 确保所有执行路径都有返回值
2. 检查函数逻辑是否完整
3. 示例修正:```c// 错误示例int func(int x) {if(x > 0) return 1;}// 修正后int func(int x) {if(x > 0) return 1;return 0;}```### 额外建议:
1. 对于复杂错误,使用 `-Wall -Wextra` 开启更多警告
2. 使用 `-Werror` 将警告视为错误,强制修正
3. 分步编译定位问题:```bashgcc -E source.c > preprocessed.c # 预处理gcc -S source.c # 生成汇编gcc -c source.c # 只编译不链接