20242817李臻《Linux⾼级编程实践》第二周
一、AI对学习内容的总结
第2章 Linux编程环境总结
1. GCC编译器
- GCC(GNU Compiler Collection)是GNU项目的一部分,支持多种编程语言,包括C、C++、Java、Fortran、Ada等。
- 编译过程:GCC将源程序转换为可执行程序的过程包括预处理、编译、汇编和链接四个步骤。
1.1 编译流程
- 预处理:处理宏定义和头文件,生成
.i
文件
gcc -E hello.c -o hello.i
- 编译:生成汇编代码
.s
文件
gcc -S hello.i -o hello.s
- 汇编:生成目标文件
.o
文件
gcc -c hello.s -o hello.o
- 链接:生成可执行文件
gcc hello.o -o hello
1.2常用选项
- 预处理控制:
-E
:仅进行预处理,输出到标准输出。-Dname
:定义宏name
,值为1。-Dname=definition
:定义宏name
,值为definition
。-Uname
:取消宏name
的定义。-undef
:取消所有与操作系统和GCC相关的宏定义。
- 编译及警告信息控制:
-o filename
:指定输出文件名。-c
:仅编译为目标文件,不进行汇编和链接。-S
:仅编译为汇编文件。-Wall
:显示所有警告信息。-W
:禁止所有警告信息。-Werror
:将所有警告信息转换为错误信息。
- C语言标准控制:
-ansi
:使用ISO C90标准。-std=c89
:使用ISO C89标准。-std=c99
:使用ISO C99标准。-std=gnu89
:使用GNU扩展的ISO C89标准。-std=gnu99
:使用GNU扩展的ISO C99标准。
- 程序调试及优化:
-g
:生成调试信息,用于GDB调试。-O0
:不进行优化(默认)。-O1
:基本优化。-O2
:进一步优化。-O3
:最高优化级别。-Os
:优化代码大小。
选项 | 功能 | 示例 |
---|---|---|
-o | 指定输出文件名 | gcc -o hello hello.c |
-c | 仅编译不链接 | gcc -c hello.c |
-Wall | 显示所有警告信息 | gcc -Wall hello.c |
-g | 生成调试信息 | gcc -g -o debug hello.c |
-D | 定义宏 | gcc -DDEBUG -o prog |
-I | 指定头文件搜索路径 | gcc -I./include |
-l | 链接库文件 | gcc -lm(链接数学库) |
1.3静态库与动态库
- 静态库:
- 使用
ar
工具创建静态库文件(.a
)。 - 静态库在编译时将代码复制到可执行文件中。
- 示例命令:
gcc -c file.c ar rcs libname.a file.o
- 使用
- 动态库:
- 使用
gcc -shared
创建动态库文件(.so
)。 - 动态库在运行时动态加载,节省磁盘空间和内存。
- 示例命令:
gcc -fPIC -c file.c gcc -shared -o libname.so file.o
- 使用
- 静态库(.a文件):
# 生成目标文件
gcc -c lib1.c lib2.c
# 打包为静态库
ar rcs libmylib.a lib1.o lib2.o
# 使用静态库
gcc main.c -L. -lmylib -o main
- 动态库(.so文件):
# 生成位置无关代码
gcc -fPIC -c lib1.c lib2.c
# 生成动态库
gcc -shared -o libmylib.so lib1.o lib2.o
# 使用动态库(需设置环境变量)
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
gcc main.c -L. -lmylib -o main
2. GDB调试器
2.1 基本使用
- GDB(GNU Debugger)是GNU项目提供的调试工具,支持C、C++、Ada、Pascal等多种语言。
- 主要功能:
- 启动程序并影响其行为。
- 根据条件停止程序执行。
- 检查程序崩溃时的状态。
- 修改程序内部状态。
- 编译时添加调试信息:
gcc -g -o debug_prog debug_prog.c
- 启动GDB:
gdb debug_prog
2.2 常用命令
- 启动调试:
file <program>
:加载可执行程序。run
:开始运行程序。run <arguments>
:传递参数运行程序。
- 断点操作:
break <location>
:在指定位置设置断点。delete breakpoints <n>
:删除断点。clear <location>
:清除断点。disable <n>
:禁用断点。enable <n>
:启用断点。
- 查看变量:
print <variable>
:打印变量值。watch <variable>
:监视变量变化。
- 程序控制:
step
:单步进入函数。next
:单步跳过函数。continue
:继续运行到下一个断点。start
:开始执行程序,在main
函数的第一条语句前停止。
- 堆栈信息:
backtrace
:查看函数调用栈。frame <n>
:查看指定堆栈帧。
- 其他命令:
list
:显示源代码。info
:显示程序状态。set var <variable>=<value>
:修改变量值。quit
:退出GDB。
选项 | 功能 | 示例 |
---|---|---|
break | 设置断点 | b main 或 b 10 |
run | 启动程序 | r |
next | 单步执行(不进入函数) | n |
step | 单步执行(进入函数) | s |
打印变量值 | p var | |
backtrace | 查看函数调用栈 | bt |
watch | 监控变量变化 | watch var |
continue | 继续运行到下一个断点 | c |
2.3 调试段错误
- 段错误调试:
- 使用
gcc -g
编译程序。 - 使用
gdb
加载程序并运行。 - 使用
backtrace
查看函数调用栈。 - 使用
info
查看程序状态。
- 使用
- 变量监视:
- 使用
watch
命令监视变量变化。 - 使用
print
命令打印变量值。
- 使用
- 生成Core Dump文件:
ulimit -c unlimited
./crash_prog
- 分析Core文件:
gdb crash_prog core
3. Make工具与Makefile
- Make工具用于自动化编译过程,通过
Makefile
文件管理项目编译规则。 - 主要功能:
- 自动检测源文件的变化。
- 自动重新编译需要更新的文件。
- 支持多目标文件的编译。
- 支持条件编译和变量定义。
3.1 Makefile基本结构
# 定义变量
CC = gcc
CFLAGS = -Wall -g
# 默认目标
all: program
# 目标依赖和命令
program: main.o utils.o
$(CC) $(CFLAGS) -o program main.o utils.o
# 隐式规则(自动推导.c -> .o)
main.o: main.c
utils.o: utils.c
# 清理规则
clean:
rm -f *.o program
3.2 核心概念
- 显式规则:明确指定目标、依赖和命令。
- 隐式规则:自动推导(如.c编译为.o)。
- 变量:简化重复内容(如$(CC))。
- 伪目标:声明不生成文件的目标(如clean):
.PHONY: clean
3.3自动变量
变量 | 含义 |
---|---|
$@ | 当前目标名称 |
$< | 第一个依赖文件 |
$^ | 所有依赖文件(去重) |
$? | 比目标新的依赖文件 |
3.4高级功能
- 条件指令:
ifeq ($(OS),Linux)
LIBS = -lpthread
else
LIBS = -lws2_32
endif
- 函数
# 查找所有.c文件
SRCS = $(wildcard *.c)
# 替换为.o文件
OBJS = $(patsubst %.c,%.o,$(SRCS))
总结
- GCC:掌握编译流程、常用选项及库文件构建。
- GDB:熟练使用断点、单步调试和Core文件分析。
- Make:理解Makefile结构、变量与自动推导,提高项目管理效率。
二、对AI总结的反思和补充
一、GCC编译器
GCC是Linux下C语言的编译器,是GNU编译器集合
1. 静态库 vs 动态库的深入对比
特性 | 静态库 (.a) | 动态库 (.so) |
---|---|---|
编译阶段 | 代码直接嵌入可执行文件 | 运行时动态加载 |
文件体积 | 可执行文件较大 | 可执行文件较小 |
部署难度 | 无需额外配置 | 需设置LD_LIBRARY_PATH 或系统路径 |
更新维护 | 需重新编译整个程序 | 替换库文件即可生效 |
典型场景 | 嵌入式开发、无依赖环境部署 | 大型软件模块化开发 |
2. 高级编译选项
- 优化级别:
gcc -O2 -o optimized_prog prog.c # 平衡性能与编译速度
gcc -Os -o size_opt_prog prog.c # 优化代码体积
- 调试符号保留:
gcc -g3 -o debug_prog prog.c # 支持宏调试(gdb中可展开宏)
3.常见问题
- 头文件冲突:
使用-I
指定自定义头文件路径,避免与系统头文件冲突:
gcc -I./custom_include -o prog prog.c
- 符号未定义:
确保库顺序正确(依赖库放后面):
gcc prog.c -L. -lfoo -lbar # libfoo依赖libbar时,libbar在后
二、GDB调试器
1.多线程调试技巧
- 查看所有线程堆栈:
(gdb) thread apply all bt
- 切换线程:
(gdb) thread 2 # 切换到线程2
2.高级断点设置
- 条件断点:
(gdb) break main.c:20 if i==5 # 当i=5时触发断点
- 临时断点
(gdb) tbreak function_name # 触发一次后自动删除
3.Core Dump深度分析
- 生成完整调用链:
(gdb) bt full # 显示完整堆栈和局部变量
(gdb) info registers # 查看寄存器状态
三、Makefile进阶
1.变量定义
- 作用:变量用于简化
Makefile
的编写,避免重复代码,提高可维护性。 - 类型:
- 递归展开变量:使用
=
赋值,变量值在使用时展开。CC = gcc CFLAGS = -Wall -g
- 简单变量:使用
:=
赋值,变量值在定义时立即展开。CC := gcc
- 递归展开变量:使用
- 使用场景:
- 在命令中通过
$(变量名)
使用变量,例如:main: main.o $(CC) -o main main.o $(CFLAGS)
- 在命令中通过
2.隐式规则
- 作用:隐式规则允许Make自动推导目标文件的生成规则,减少
Makefile
的编写工作量。 - 示例:
- 将
.c
文件编译为.o
文件:%.o: %.c $(CC) -c $< -o $@ $(CFLAGS)
- 使用隐式规则简化目标文件的生成:
main: main.o hello1.o hello2.o $(CC) -o main main.o hello1.o hello2.o $(CFLAGS)
- 将
3.伪目标
- 伪目标:
- 作用:伪目标用于执行特定命令,避免与同名文件冲突,提高
Makefile
的灵活性。 - 常见伪目标:
all
:用于编译所有目标。clean
:用于清理生成的文件。install
:用于安装生成的程序。
- 示例:
.PHONY: all clean install all: main clean: rm -f *.o main install: cp main /usr/bin
- 作用:伪目标用于执行特定命令,避免与同名文件冲突,提高
三、学习思维导图
第二章
mindmap
root((Linux编程环境))
GCC
编译器简介
将源程序转换为可执行程序
预处理、编译、汇编、链接
常用选项
预处理控制
-E选项
-Dname选项
编译及警告信息控制选项
-o选项
-c选项
-Wall选项
C语言标准控制选项
-ansi选项
-std=选项
搜索路径控制和链接选项
-Idir选项
-Ldir选项
创建库文件
静态库
动态库
GDB调试器
使用方法
编译时添加-g参数
启动GDB
常用命令
list
break
run
display
step
print
continue
quit
调试实例
段错误调试
程序错误分析
Make工具
Makefile编写
显式规则
变量定义
注释
Makefile示例
项目结构
编译规则
Make命令用法
-f选项
-n选项
-d选项
四、基于AI的学习
1.关于Makefile
- 我:
我在学习Make和Makefile知识点,请你以苏格拉底的方式对我进行提问,一次一个问题 - AI:
你知道什么是Makefile,以及它在软件开发中起什么作用吗? - 我:
Makefile 是一个用来告诉计算机如何编译和构建程序的文件,它和一个叫 make 的工具一起工作。它可以实现自动化编译,方便管理项目。 - AI:
既然Makefile可以实现自动化编译,那么你认为为什么我们需要自动化编译?手动编译有哪些局限性? - 我:
自动化编译可以更方便地管理项目,手动编译耗时费力,同时人工的操作会增加编译时间 - AI:
既然自动化编译可以提高效率,那么你认为一个典型的Makefile应该包含哪些关键元素,才能实现有效的自动化编译? - 我:
你的理解是什么 - AI:
目标(Targets):指定要生成的文件或执行的操作,比如编译生成的可执行文件或目标文件。
依赖(Dependencies):列出目标所依赖的文件或其他目标,确保在目标构建之前这些依赖已经准备好。
命令(Commands):定义如何从依赖生成目标的具体命令,通常是编译或链接指令。
你认为为什么需要将目标、依赖和命令分开定义?这样做有什么好处? - 我:
可以让项目具有清晰的逻辑结构,使得开发更加灵活,方便团队协作。针对我该知识点,我理解了吗?我的回答结束了,请对我的回答进行评价总结 - AI:
优点:
理解基本功能:你知道Makefile用于自动化编译,并与make工具协同工作。
认识自动化优势:你提到自动化编译节省时间、减少人工错误。
理解结构:你认识到Makefile包含目标、依赖和命令,有助于项目管理和团队协作。
可深入的地方:
依赖关系:可以更具体说明Makefile如何通过时间戳检查避免不必要的重新编译。
高级特性:探索变量、模式规则、条件判断等,编写更灵活的Makefile。
实践:通过实际项目练习,直观理解其工作原理。
总结:
你对Makefile的基础掌握不错,接下来可以通过实践和学习高级特性进一步提升。继续加油!有问题随时提问。
2.关于GDB
- 我:
我在学习GDB知识点,请你以苏格拉底的方式对我进行提问,一次一个问题 - AI:
你知道GDB是用来做什么的吗?请简要描述它的主要功能。 - 我:
GDB是一款调试功能,能帮助程序员调试程序。 - AI:
你认为在调试程序时,GDB能够帮助程序员完成哪些具体的任务?或者说,GDB提供了哪些功能来辅助调试? - 我:
设置断点、单步执行、查看变量、查看调用栈、修改变量等 - AI:
你能详细解释一下如何在GDB中设置断点吗?具体有哪些方法可以设置断点? - 我:
1、break 文件名:行号
2、break 函数名
3、break *地址
4、break 文件名:行号 if 条件
5、tbreak 文件名:行号 - AI:
你提到可以使用 break 文件名:行号 if 条件 来设置条件断点。你能举一个具体的例子,说明在什么情况下使用条件断点会特别有用吗? - 我:
假设你在调试一个程序,想找出变量 x 是何时变成负数的。你可以设置一个条件断点:
break 文件名:行号 if x < 0
这样,程序只有在变量 x 小于 0 时才会暂停,其他时候不会停。这样就能快速定位问题发生的位置,而不需要每次都手动检查变量值。
-
AI:
在你提到的例子中,假设你已经设置了条件断点,并且程序在某个时刻暂停了。接下来,你会使用哪些GDB命令来进一步分析问题,并找出变量 x 变成负数的原因? -
我:
使用print加设置断点、单步执行的方法,逐步排除出错的地方。我的回答结束了,请对我的回答进行评价总结。 -
AI:
你的回答非常简洁,并且抓住了关键点!以下是对你回答的总结和评价:
- 明确目标:你清楚地指出了使用
print
命令来查看变量的值,这是调试过程中非常基础且重要的一步。 - 逐步排查:你提到了通过设置断点和单步执行来逐步排查问题,这是一种非常系统化的调试方法,能够帮助精确定位问题的根源。
- 简洁有效:你的回答虽然简短,但涵盖了调试的核心步骤,体现了你对GDB调试流程的基本理解。
改进建议:
- 你可以进一步扩展一下具体的GDB命令,例如:
print x
:查看变量x
的当前值。step
或next
:单步执行代码。backtrace
:查看调用栈,了解程序是如何执行到当前断点的。info locals
:查看当前函数的所有局部变量。
- 另外,可以结合具体场景,描述如何通过这些命令逐步缩小问题范围,最终找到
x
变成负数的原因。
总的来说,你的回答已经抓住了调试的核心思路,继续保持这种系统化的思维方式,并在实践中不断积累经验,你会越来越熟练地使用GDB进行调试!
五、基于AI的代码实践
使用Gitee进行代码托管
仓库链接:
https://gitee.com/li-zhen1215/homework/tree/master/
例2-1
hello.c
#include<stdio.h>
void main()
{
printf("hello,world\n");
}
实践展示:
例2-2
debug.c
【例 2-2】GCC -D 选项的使用,假设程序 debug.c 内容如下:
#include <stdio.h>
void main()
{
int arr[10],i=0;
for (i=0;i<10;i++)
{
arr[i]=i;
if (DEBUG) // 使用了一个名为 DEBUG 的宏,该宏在编译的时候定义
{
printf("arr[%d]=%d\n",i,arr[i]);
}
}
}
实践展示:
例2-3
test.c
#include <stdio.h>
#define sum(a,b) a+b // 宏定义
void main()
{
int num=sum(1,2);
printf("num=%d\n",num);
}
gcc -E -o test.i test.c
实践结果:
test.i:
例2-4
main.c
#include<stdio.h>
#include<math.h>
int main()
{
printf("hello world\n");
double pi=3.1415926;
printf("sin(pi/2)=%f",sin(pi/2));
return 0;
}
例2-5
calc.c
double aver(double,double);
double sum(double,double);
aver.c
#include "calc.h"
double aver(double num1,double num2)
{
return (num1+num2)/2;
}
sum.c
sum.c 文件实现第二个函数:
#include "calc.h"
double sum(double num1,double num2)
{
return (num1+num2);
}
实践结果:
例2-6
main.c
#include <stdio.h>
#include "calc.h"
int main(int argc, char* argv[]) {
double v1, v2, m, sum2;
v1 = 3.2;
v2 = 8.9;
m = aver(v1, v2);
sum2 = sum(v1, v2);
printf("The mean of %3.2f and %3.2f is %3.2f\n", v1, v2, m);
printf("The sum of %3.2f and %3.2f is %3.2f\n", v1, v2, sum2);
return 0;
}
实践结果:
例2-7
example.c
(3)GDB 常用命令详解
【例 2-7】下面以 example.c 源程序为例,演示 GDB 的基本调试过程,首先使用命令 gcc -o -g exam example.c 编译生成可执行程序。
#include <stdio.h>
void debug(char *str)
{
printf("debug information: %s\n", debug);
}
main()
{
int i,j;
j=0;
for (i=0;i<10;i++)
{
j+=5;
printf("now a==%d\n",j);
debug("xxxxxxxxxxxx");
}
}
然后在同一目录下输入 GDB 命令,在出现 GDB 的提示符后,执行 file exam 命令,加载 exam 程序以进行调试。如下所示:
(gdb) file exam
Reading symbols from /home/cosmos/book/chapter2/exam...done.
实践结果:
例2-8
#include <stdio.h>
#include <stdlib.h>
void recurse(void)
{
static int i;
if ( ++i == 3 )
abort();
else
recurse();
}
int main(int argc,char **argv)
{
recurse();
}
实践结果
例2-9
test.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h> // 确保包含了 string.h 头文件
int main() {
char string[256]; // 定义一个足够大的数组来存储输入的字符串
printf("Please input a string: ");
fgets(string, sizeof(string), stdin); // 使用 fgets 代替 gets
string[strcspn(string, "\n")] = 0; // 去除换行符
printf("\nThe string is: %s\n", string);
return 0;
}
实践截图:
例2-10
hello1.h
/*hello1.h*/
#ifndef HELLO_1_H // 预编译命令,防止多次包含头文件
#define HELLO_1_H
void hello1(char *mess); // 打印 mess 的内容
#endif
hello2.h
/*hello2.h*/
#ifndef HELLO_2_H // 预编译命令,防止多次包含头文件
#define HELLO_2_H
void hello2(char *mess); // 打印 mess 的内容
#endif
hello1.c
#include "hello1.h"
#include <stdio.h>
void hello1(char *mess) {
printf("This is hello1 print %s\n", mess);
}
hello2.c
#include "hello2.h"
#include <stdio.h>
void hello2(char *mess) {
printf("This is hello2 print %s\n", mess);
}
main.c
/*main.c*/
#include "hello1.h"
#include "hello2.h"
int main(int argc, char **argv)
{
hello1("hello"); // 执行 hello1 函数
hello2("world"); // 执行 hello2 函数
}
Makefile
#Makefile
main:main.o hello1.o hello2.o
gcc -o main main.o hello1.o hello2.o
main.o:main.c hello1.h hello2.h
gcc -c main.c
hello1.o:hello1.c hello1.h
gcc -c hello1.c
hello2.o:hello2.c hello2.h
gcc -c hello2.c
clean:
rm main hello1.o hello2.o main.o
实践截图:
课后习题
1. 静态链接库和动态链接库之间的相同点和不同点
相同点:
- 都是共享代码的方式,避免重复编写和编译相同的代码。
- 允许程序使用库中定义的函数和变量。
不同点:
- 静态链接库在编译时被复制到最终的可执行文件中,增加可执行文件的大小,运行时不需要库文件。
- 动态链接库在程序运行时加载,减小可执行文件的大小,运行时需要库文件。
- 静态链接库更新需要重新编译所有使用该库的程序;动态链接库更新只需替换库文件,无需重新编译程序。
2. Make命令工具如何确定哪些文件需要重新生成,而哪些不需要生成
- Make工具通过比较目标文件和其依赖文件的时间戳来确定。
- 如果目标文件不存在或其修改时间晚于任一依赖文件,则需要重新生成。
- 如果目标文件比所有依赖文件新,则不需要重新生成。
- Makefile中的规则和变量也会影响哪些文件需要重新生成。
3. Make中的伪目标的作用是什么
- 伪目标用于定义不对应实际文件的目标。
- 用于执行操作,如构建多个目标或执行复杂构建步骤。
- 避免与实际文件名冲突,确保Make能执行所需操作。
- 伪目标通常以
.PHONY
声明,例如.PHONY: clean
,确保make clean
执行清理操作。
4./5.用隐式规则实现图示题目
Makefile
# 目标文件
TARGET = menu
OBJS = menu.o music.o picture.o
# 编译器
CC = gcc
CFLAGS = -Wall -g
# 生成可执行文件
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $(TARGET) $(OBJS)
# 生成 .o 文件的规则
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# 清除编译文件
.PHONY: clean install
clean:
rm -f $(TARGET) $(OBJS)
# 安装可执行文件
install: $(TARGET)
cp $(TARGET) /usr/bin/
menu.c
#include <stdio.h>
#include "menu.h"
int main() {
printf("Menu module\n");
music();
picture();
return 0;
}
menu.h
#ifndef MENU_H
#define MENU_H
void music();
void picture();
#endif
music.c
#include <stdio.h>
#include "menu.h"
void music() {
printf("Music module\n");
}
picture…c
#include <stdio.h>
#include "menu.h"
void picture() {
printf("Picture module\n");
}
实践结果:
六、学习实践过程遇到的问题与解决方式
问题1,不太会用Gitee仓库
向AI提出问题:
给我生成一份详细的Gitee使用教程,在Ubuntu系统上。
根据DeepSeek给出的教程,结合CSDN上的博客,一点点进行实操。
第一次上传的时候,直接把当前目录的所有文件上传到了仓库,显得很混乱。
后面向AI了解了git的一些概念,成功解决问题。
问题2,不习惯使用Makefile的自动化编译
使用Makefile时经常出现各种错误,比如链接库失败,依赖缺失等。当程序报错的时候,可以将报错信息发给AI,AI会分析报错原因,并提供正确的做法,如:
编译错误:undefined reference to xxx
链接错误:library not found
依赖问题:missing dependency
AI还会生成各种学习资源,以及对当前更好的修改方法。而且AI可以生成基础语法教程,如果对哪一种命令感到疑惑,也可以问AI,让AI生成具体用法并给出示例,学习事半功倍。
参考资料
-
AI工具(你使用的AI工具及其链接)
- 问小白
- Kimi
-
图书
- 《Linux编程基础.李养群》电子版
-
网站
- CSDN
“20242817李臻 原创作品转载请注明出处 《Linux高级编程实践》”