快捷键或命令 作用
ctrl+ alt + t 打开终端窗口
tab 命令补全,补全命令,补全目录、补全命令参数
ctrl + c 强行终止当前命令
ctrl + d 键盘输入终止
ctrl + s 暂停命令,任意键继续
ctrl + z 将当前程序放到后台运行,恢复到前台为命令fg
ping + 网址 检测网络
ifconfig 查看网络默认分配地址
clear 清除界面
ping + 默认分配ip地址 检测与路由器连接状况
ping + 127.0.0.1 检测与本机连接情况
sudo(获取超级命令权限) apt install(下载安装命令) … 安装…
sudo apt update 更新软件列表
cat /etc/… 查看…
cat /etc/issue 查看发行版本
history 查看历来命令(从你装了这个系统开始)
man … 查看某命令的相关命令及解释
cd / 切换到根目录,cd用于改变当前工作目录 要加路径
ll 显示当前目录的文件(附详细信息)
ls 罗列当前目录的文件
pwd 显示当前工作目录
cd . 返回当前目录
cd .. 返回上层目录
cd ~ 切换的该用户家目录
cd - 返回上一次的目录cd
cd /…/ 进入…的目录
ls(后面的符号i与cd相同) 显示指定目录的内容
mkdir xxx (make directory)在当前目录下创建文件夹,也可一次性创建多个,即mkdir xxx xxx xxx …
rmdir xxx 删除当前目录的xxx文件夹
rm xxx(或者对应路径下的xxx) 删除当前目录的xxx文件
cp file1 file2 将某目录file1文件复制到某目录file2文件下,如果存在file2则覆盖,如果不存在则创建,-r时可以操作文件夹
mv file1 file2 将file1移动到file2所在位置,如果在同目录下且file2不存在,则可认为是将file1改名为file2,如果file2不存在则创建文件,如果文件存在则覆盖文件
ln aaa bbb 硬链接bbb链接到aaa
ln /…/aaa bbb -s 软链接bbb存储指向aaa的路径
echo “aaabbb” > xxx 把字符串aaabbb写入到xxx文件中
find -name “xxx” 查找指定xxx名称的文件的路径
chmod xxx file 数字设定法改变一个文件对三类用户的权限
umask 查看文件掩码
umask xxxx 临时修改当前用户文件掩码

初步认识linux

网络设置

桥接模式:处于同一个网络,路由器给主机分配一个ip地址也给虚拟机分配一个ip地址

NAT模式:本机虚拟一个路由器,该路由器用来连接虚拟机,即创建了一个内网

内网可以访问外网,外网无法访问内网(一般情况,NAT穿透技术可以实现外网访问内网)

设置静态IP地址

  1. 选择自动DHCP模式

2.重启网络(网络打开再关闭),检测有无网络

3.查看网络detail信息,记录下来

4.在ipv4分页改为手动

5.填写记录信息,子网掩码设置为255.255.255.0,子网掩码用于获取ip地址的网段,网关填写默认路由地址

6.应用设置,测试网络

远程连接(连接虚拟机)

1.C/S架构,客户端(client)服务器(sever)架构,如果连接虚拟机,服务器就是虚拟机,客户端即远程连接工具所在机器,服务器端需要安装服务端程序(sshd)

 $sudo apt install ssh
//安装ssh客户端
$ps -elf|grep sshd
//确认ssh客户端是否运行,如果有根目录就说明安装成功

2.切换到客户端xshell程序,open命令打开会话,新建会话

3.协议选择ssh,主机名为虚拟机静态ip地址

建立一个c/cpp文件并编译运行

$ cd hello.cpp  #进入源文件所在目录
$ touch hello.c #新建空白的源文件
$ gedit hello.c #编辑源文件
$ gcc hello.c #生成可执行程序
$ ./hello.out #运行可执行程序
hello
$ #继续输入其它命令

linux内核

内核(kernel)作用:

​ 1.管理硬件资源

2.为上层应用软件提供了运行环境

系统调用(system calls) 内核对上层应用程序提供的的接口

库函数 对系统调用进行的包装

shell 命令解释器,解析命令,执行命令/脚本,脚本(命令的集合)

用户子系统

用户分类

1.特权用户/root用户 啥都能干

2.普通用户 sudoers 临时拥有一些权限

3.其他用户

查看所有用户命令

$ cat /etc/passwd

为某用户申请特权

$ sudo useradd xxx

删除用户及其所拥有文件

$ sudo userdel xxx
//并删除其目录
$sudo userdel -r xxx

正确创建用户

$ sudo useradd -m -s bin/bash xxx
//-m表示自动创建家目录
//-s表示指定路径下,比如当前命令为指定bash为默认的shell

切换用户

$su xxx
//切换到root时因为每次开机都会随机root密码,所以需要用命令
$sudo passwd root来两次更改新root密码

banner打印字符串

//一般打印
使用banner xxx打印xxx

//指定参数打印
//竖向
printerbanner -w len xxx
//-w表示打印指定宽度,len为指定宽度

//横向
toilet -w len xxx
//多次打印知道ctrl+c结束
figlet -w len xxx xxx xxx xxx ... (ctrl + c)

xxx的这种参数一般可放在命令末尾也可以放在主命令后面

文件操作

文件子系统

文件名 意义
bin binary 可执行程序
dev device 设备文件
home 普通目录家目录的根目录
root root用户的家目录
sbin system binary 和系统相关的可执行程序
var variable 经常发生变化的文件(e.g. 日志文件)
etc 配置文件
lib 库文件
proc process 进程映射文件
usr 普通用户能够访问的文件

文件夹与文件操作

$mkdir xxx
//在当前目录下创建xxx文件夹

$mkdir xxx xxx xxx xxx
//在当前目录下创建多个文件夹

$mkdir xxx -v
//创建的同时得到创建的详细信息

$rmdir xxx
//与mkdir操作相同,不过从创建变为了删除,且删除的只能是空目录空文件夹

$mv aaa bbb
//将aaa文件移动到bbb文件夹内

$ll
//完全显示当前目录的文件(隐藏文件也可显示,附带详细信息)

$ls
//显示当前目录文件

$ll/ls -a
//-l表示显示文件的详细信息,-a显示当前目录所有文件 -t表示以时间排列显示文件,-i表示显示id,这些参数可以组合使用,h表示将文件大小以人类易读的方式显示
//例如 ls -li

在ll或者ls - l显示文件时

第一列如果前缀时dir则表示文件夹,如果是-开头则表示普通文件,l开头则表示符号链接,c代表字符设备(键盘),b代表块设备(硬盘),p代表管道文件(进程之间进行通信的文件),s表示套接字文件(网络通信)

后面跟着的w,r,e分别代表write,read,execute,表示普通用户能够行使的权限,总共出现三次即三组,第一组表示自己的权限,第二组表示同组的其他成员的权限,第三组表示其他组成员的权限,例如 drwxr-xr-x

第二列 硬链接个数

第三列 用户名称

第四列 用户所属组名

第五列 文件大小 单位比特byte

第六列 最近修改时间

最后一列 文件名称

通配符(wild card)

通配符 意义
* 可以匹配任意多个字符(包括0个字符)
可以匹配任意一个字符
[…] 匹配这个集合(即这个括号内)的任意一个字符
[!…] 匹配集合外的任意一个字符
[0-9],[a-z],[A-Z],[A-Za-z] 匹配内部的任意一个字符

cp(copy)

//可以复制一个文件或一个文件夹
cp /.../xxx .
//把某路径下的xxx复制到当前目录下,'.'也可以换成某个路径

cp file1 file2
//把文件file1复制到另一个文件file2中
//如果文件file2不存在,则创建这个文件,如果文件存在则覆盖这个文件
//-i会诊断要覆盖文件原本是否有信息,并提示是否覆盖,-r表示递归地复制文件夹,-u表示复制多个文件到某个目录下时,会选择性的进行复制,如果某个文件不在这个目录下或者某个文件的修改时间新于该目录下同名文件,则会将这个文件复制或覆盖到该目录下,-v表示复制时的详细信息
//-r的时候,如果file2文件夹存在,则会将file1文件夹复制到file2目录下
cp *.cpp dir
把通配符后带有.cpp的文件全部复制到dir目录中

mv

//将一个文件移动到另一目录下
mv file1 file2
//将file1移动到file2所在位置,如果在同目录下且file2不存在,则可认为是将file1改名为file2
//-i表示如果同名是提示是否覆盖

mv file dir
//如果dir存在,则将file移动到dir中,如果不存在,则认为dir是一个普通的文件,并创建
mv dir1 dir2
//如果dir2存在,则将dir1文件夹移动到dir2中,如果不存在则会创建文件夹,将dir1内的文件复制到dir2中,如果是同目录则起到重命名的效果
//-v,-u与cp的-v,-u相同效果

rm

//删除文件或者删除文件夹(与rmdir不同,既可以删除空文件夹也可以删除有文件的文件夹)
rm /.../ xxx xxx xxx
//删除xxx或多个文件
//-i提示要不要删除,-r递归删除即能删除文件夹,-v删除的详细过程,-f忽略提示
rm-rf / 删库跑路
//自己删除时建议不要使用root用户进行删除,删除之前要通过ls确认要删除的选项,同时添加-i参数

链接

硬链接

目录本质也是一个文件

通过ls - la可以看到开头的两个文件分别是 . 和 ..

目录会存储一些目录项,. 和 .. 就是目录项,分别指向当前目录和上级目录,目录项以链表进行链接,每个节点就是一个目录项(entry),目录中的普通文件不算目录项

通过目录项可以直接访问某个目录,就称为硬链接

如果想在一个目录中添加或删除目录项,该用户需要拥有该目录的写权限

一个普通文件的硬链接数默认为1

软链接(符号链接)

某个文件存储的是一个路径,这条路径指向了一个目录,这个间接访问目录的方式就成为软链接

符号链接类似与指针,和windows的快捷方式

ln(link)

//创建链接
ln aaa bbb
//创建一个链接到aaa的目录项,目录项名为bbb

ln /.../aaa bbb
//创建一个链接到某目录的aaa文件的bbb软链接,例如ln ../aaa bbb意为bbb的文件存储了指向上级目录aaa文件的路径,且以..或.等目录项写的路径成为相对路径,具体描述则为绝对路径 ln /home/niepandou/aaa bbb
//为了避免文件转移导致的绝对路径找不到文件,一般都选择相对路径进行描述

查找文件

locate

//全盘查找某文件
locate xxx
//查找所有与xxx相关的文件,如果某个文件已经在linux的数据库中,查找起来会非常快
//如果是新创建的一个文件,暂时是查不到该文件的

which(常用)

//定位一个command(可执行程序)
which xxx
//which sshd可查找到sshd程序的位置

find

//在该目录下进行搜索,如果有子目录也会在子目录中进行搜索, 可以使用通配符进行查找
find xxx
//-name表示以名字(-name 具体名字)进行查找,输出的是他的路径
//-a 表示逻辑与and,-o表示或or,!表示取反

组合查找
find /.../ -name "*.a" -o -name "*.b"查找以.a或.b结尾的文件

//-gid n表示查找属于id号为n的用户组的所有文件
//-uid n表示查找属于id号为n的用户的所有文件 uid也可写为user
//-type c表示查找类型为c的文件 b块设备文件,c字符设备文件,d文件夹(目录)文件,p管道文件,f普通文件,l软链接,s表示套接字文件,硬链接文件可以看作是一个普通文件

e.g.: find ~ -name "*hello*" -a -type f//在家目录中寻找包含hello且为类型为普通文件的文件

//-size n表示以存储单元查找 b表示块,一般512字节称之为块,c(character)表示字节,K,M,G则为其原来的意思,默认查找块
e.g: find ~ -size 8980c //精确查找字节为8980的文件 +8090c则表示查找大于8090c的文件,如果有小数要记住查找是向上取整

//如果要查找0字节则选择 -empty(可以用来查找空文件和空目录)

//-perm表示这个文件对不同用户的权限,-rw-rw-r--每一组用二级制表示为110 110 100(开头的-表示普通文件),因为一个用户最多用的的权限有rwx,二进制转十进制为7,所以每一组可以用八进制表示,-perm就是通过三个八进制数来作为指标进行find查找的,-rw-rw-r--用三个八进制表示为664,则可以写为-perm 664
//一般普通文件的perm就是664
//如果查找是通过用户的单个权限 也可以这样写 -perm -u=x 表示的是查找用户拥有执行权限的文件

//-exec 可以以行为单位把一个命令的输出结果当作另一个命令的参数,一般可用于组合命令
find -name “* .cpp” -exec ls -l {} \; //{} 将find命令输出的结果以行为单位作为ls -l的参数

权限

一般用linux执行.py文件时是没有足够权限的

在执行该文件时提示权限不足

./hello.py
Permisson Denied

chmod(change mode)改变权限

//文字设定法
chmod [ugoa][+=-][rwx] file/dir//第一组表示哪类用户,a表示all,第二组表示增加减少某个权限, =时表示该用户只有某个权限,第三组表示的就是具体权限
组合修改权限
e.g: chmod u=rw, g=rw, o=r hello.py
//数字设定法
chmod [三个八进制数] file/dir //第一组分别表示ugo三类用户的权限

umask(文件掩码)

文件掩码指的是在创建文件(0666)或目录时(0777)在全部权限中要去掉的一些权限

//获取文件掩码

直接输入umask会得到0002002为普通用户的文件掩码,0022为root用户掩码
创建文件夹时,默认掩码为777,即所有用户都拥有所有权限,最终得到的却是775,最后得到的文件掩码775 = 777 & (~umask)
创建普通文件时,默认掩码为666,最终文件掩码为664 = 666 & (~0002
计算的过程可以直接简化成 666 - umask

//临时改变当前用户的umask
umask xxxx//umask 0222

查看文件

文件描述符(一般为非负整数)

stdin 标准输入 0

stdout 标准输出 1

stderror 标准错误输出 2

‘>’ 标准输出重定向(可以认为是c++的输入)

’<‘ 标准输入重定向(可以认为是c++的输出)

2 > 标准错误重定向(2就是strerror)

‘>>’ 标准输出重定向(追加方式)

cat(查看文件内容)

//查看一个文件的内容(速度慢,因为和strcat一样是在输出流直接拼接内容)
cat file

//将file1文件的内容输出到file2中,类似cp
cat file1 > file2

//将键盘输入的内容输输出(写入)到file文件中,如果不写file则是输出到显示器上,ctrl + D结束输入
cat > file
//下次再对这个file进行相同操作时是重新写入, >>则会追加写入

echo(打印一行文本)

//打印xxx
echo "xxx"
//如果echo后面不带任何东西,则会打印一行空行

//将文本xxx输出(写入)到file文件中,同样是重新写入
echo "xxx" > file

head(显示文件的头几行信息)

//默认输出10行

//打印file文件前n行
head -n n file

tail(显示文件的后几行信息)

//默认输出10行

//指定n行与head相同
tail -n n file

//可以用于查看最近日志文件

more/less(单页浏览文件)

//每次查看文件的某页

//查看a文件的首页,f(forward)浏览下一页,b(backward)浏览上一页,q(quit)退出
more a.txt

正则表达式

sort

//对文件内容进行排序(以行为单位)

//对file文件排序,比如文件内第一行为bbb,第二行为aaa,sort之后则打印aaa,第二行bbb,原本的内容不会发生改变
sort file

//不能够文件重定向自己,这样做会使文件内容清零
sort file > file
//cat sort会发现没有任何输出

//但可以文件重定向另一个文件
sort file1 > file2

uniq(unique)

//去重连续重复的几行

//对file文件读取并以行为单位去重,打印,原本的内容不会改变
unique file

对一个文件操作得到排序且没有重复元素的文件

sort sort.txt > sortt.txt;uniq sortt.txt > sort.txt;rm sortt.txt

xargs

// | 管道连接两个命令,管道相当于一个缓冲区,通过第一条命令将结果输送到管道中,然后对管道的内容执行第二条命令,xargs是对内容每一行执行一次命令2

//命令1输出的每一行都执行一次命令2
commmand1 | xargs command2
e.g: find ~ -name "*.cpp" | xargs ls -l

//对命令1得到的结果整体进行命令2
command1 | command2
e.g : sort sort.txt | uniq

file

//得到一个文件的信息(比ls-la更详细)

ls file

wc(what count)

//记录文件中的行数,字节数,单词数

//记录文件中有多少个c
wc file -c
//-c表示字节数
//-l表示行数
//-w表示单词书

iconv

//修改字符集

iconv -f gbk -t uft-8 xxx
//将xxx文件的字符集从gbk转译成uft-8

正则表达式

基本单位:普通字符,转义字符,’.’,(任意一个字符),[] 集合(只要集合里的一个元素能匹配上就是能匹配 ),()这个整体作为基本单位

基本操作:1.连接:ab

​ 2.重复(必须是连续的):’?‘ 表示重复1次或0次

​ ’+‘ 表示重复一次或多次

​ ‘*’ 表示重复任意次数

​ {m,n}表示重复m-n次,m和n可以有一个没有,m没有可以认为最多重复n次,n没有可以认为至少重复m次

​ {n}表示连续出现特定n次

​ [ ^abc]匹配任意字符,但不包含a,b,c

特殊符号:

​ ^行首 “^abc”

​ $行尾 “abc$”

​ \<词首 “\<a”

\\>词尾 "a\\>"

gerp(搜索文件内容)

//globally regex(regular experssion) print
//以行为单位进行查找
grep -en "bbb" aaa//在aaa文件中搜索带有bbb文本的内容
//-e 使用扩展的正则表达式,一般组合使用放在最后
//-n 显示行号
//-i 忽略正则表示的大小写

e.g:grep -ne "cpp" test.txt
grep -ne "h\{1\}" test.txt

find ~ -name "*.cpp" |xargs grep -ne "int main\(\)"

alias

alias//别名

alias
//显示目前所有已有的别名
alias aaa=bbb
//给bbb起一个别名为aaa,临时生效

打包和压缩

//打包
打包文件一般比原文件大,打包文件会预留空间

tar(主选项+辅选项) 目标文件名 源文件或目录
主选项:只能选择其中一个
c(create)r(追加)x(释放)
辅选项:
f(生成指定使用包文件或设备,指定文件名称)
v(显示打包过程)
z(用gzip压缩/解压缩文件,后缀名为.gz)

e.g:tar cvzf a.gz a.out//创造a.gz压缩文件将a.out放进该压缩文件中
tar xvf hello.gz//解压缩hello.gz文件,解压的文件放到该目录下
//注意参数没有-

scp

df(disk full查看磁盘状态)

-h增加可读性

du(disk used查看磁盘使用情况)

du /.../ 查看某目录下的使用情况,默认为当前目录

-h同上
-d n 最多显示n个目录层数

du -h -d 1 //显示当前目录下文件和目录所占空间
du -h -d 2//显示当前目录及子目录下文件和目录所占空间

scp(s_cp远程拷贝secure copy)

//在网络上传下载

scp source dest

本地路径:绝对路径,相对路径

远程路径:用户名@IP地址:+路径

上传
scp Debugger.pdf niepandou@192.168.8.130: ~//将debugger文件传输给远程用户的家目录下
下载
scp -r niepandou@192.168.8.130:~/cpp .//将远程用户的家目录下的cpp文件夹下载到当前目录下

//-r 复制文件夹


上述操作每次操作时都需要验证用户密码,使用密钥后可以跳过该过程

ssh-keygen密钥

产生密钥

id-rsa密钥
id-rsa-pub公钥

用scp将公钥传给远程用户,然后将pub文件放进ssh目录下(注意采用>>追加)的authorized_keys文件

之后每次访问都不需要密码验证

git仓库

修改远程仓库过程

1.修改本地仓库(远程仓库文件传到本地仓库)
2.add(将修改添加到缓存中,stage操作)
3.commit(确认修改)
4.push(将修改内容传到远程仓库)
如果有多人同时进行修改,系统会检验当前文件是否为原先的文件,如果不是则不会进行修改

git用法与scp用法相同

代码编译

编辑器

vim

vim三种模式
短命令

普通模式(命令模式)【ESC切换】
编辑模式(插入模式)【INSERT切换】
i插入到光标的前面,a插入到光标的后面(append),o光标切换到下一行
I行首,A行尾,O前一行
视图模式

命令模式:
长命令
':'开头
wq写入并退出

光标移动
上下左右键 kjhl
翻页 ctrl+f(forward),ctrl+b(backword)
翻半页 ctrl+u(up)ctrl+d(down)
H:页首
L:该页最后一行的行首
gg:文件的开始
G:文件的末尾
w;下一个单词(word)
b:下一个单词(back)
n- 往上走n行
n+ 往下走n行
nG / :n 到第n行


删除操作(相当于剪切)
x:删除一个字符
dd:删除一行
ndd 删除光标往下 n行
:n,md:删除n-m行
d^:删除该行光标到行首部分
d$ / D:删除该行光标到行尾部分
dw:删除一个单词
[n]dw:删除n个单词
d[n]w:删除n个单词
dt):删除到)
dt":删除到“
u,撤销上一次操作undo
crtl+r:恢复修改 recover
p:粘贴操作(paste)
yy:拷贝一行(yank)
[n]yy / y[n]y:拷贝n行
:x,y y拷贝x-y行
yw:拷贝一个单词
[n]yw / y[n]w:拷贝n个单词

查找替换
:set hlsearch高亮显示
/xxx 查找xxx(配合高亮显示使用更佳)(也可使用正则表达式匹配)
匹配到后 n移动到下一个匹配项,N移动到下一个匹配项

:s/aaa/bbb/选项(aaa替换为bbb,默认情况下只会替换第一个)
选项为g表示global全部替换
:x,ys/aaa/bbb/选项(将x到y行根据选项对aaa替换为bbb)
:%s/aaa/bbb/g(将该文件内所有aaa替换为bbb)
插入模式:
i插入到光标的前面,a插入到光标的后面(append),o光标切换到下一行
I行首,A行尾,O前一行
视图模式(用于选择范围):
v:行选模式
ctrl+v:列选模式
选完后d删除y拷贝

批量/批量删除注释
1:将光标移动到要注释的第一行
2:进入列选模式选中范围
3:进入插入模式(I)
4:输入//
5:回到命令模式
删除时在列选模式下用d删除//

gg=G 代码对齐命令

文件操作
:w 保存
:q 退出
:q! 不保存强制退出
:wq 保存修改并退出

:new / split / sp file 打开新的窗口
ctrl+w移动到下一个窗口
:qa 退出所有窗口
:vnew / vsplit / vsp 左右分屏

:tabnew 标签分屏(像浏览器一样的标签)
gt 下一个标签 gT下一个标签

:set number 显示行(每次退出再重进会刷新)


设置vim配置文件

配置文件类似于预处理

1.在家目录下创建.vimrc文件

2.打开.vimrc文件

常用配置命令

syntax on//语法检查
set hlserach//查找高亮显示
set tabstop=4//tab缩进为4个空格,默认8个
set autoindent//自动对齐

vimtutor vim练习手册

编译工具链

两种大编译环境

ide: 集成开发环境(常用于windows),例如vs,clion,eclipse,xcode

sdk(常用于linux, 全程software development kit,软件开发工具或编译工具链)

​ sdk阵营一: gcc

​ sdk阵营二: clang

gcc -v 查看gcc版本
--target=x86_64-linux-gnu //此为gcc对应平台架构
Thread model: posix //模型标准
gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04) //gcc版本

一个程序从创建到可执行的过程

image-20230507185757060

预处理

预处理文件常以.i结尾
作用:执行预处理指令
#include
#define
#define xxx function 宏函数
#if
#else
#endif
gcc -E xxx.c -o xxx.i 生成预处理文件
E表示进行预处理 -o表示生成文件的名字
-D xxx 再定义一个宏xxx

源代码

#include <stdio.h>

int main()
{
#if 0//宏if
printf("Hello World");
#else
printf("hello world");
return 0;
#endif //宏if结束
}

生成预处理文件

$gcc -E define.c -o define.i

文件内容

...//以上省略各种预处理代码
extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;


extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
# 868 "/usr/include/stdio.h" 3 4

# 2 "define.c" 2


# 3 "define.c"
//最终代码
int main()
{



printf("hello world");
return 0;

}

假设源代码将#if 0改为#if N

#include <stdio.h>

int main()
{
#if N//宏if
printf("Hello World");
#else
printf("hello world");
return 0;
#endif //宏if结束
}
  • 因为N是不存在的

    所以生成的预处理文件会如此

    //最终代码
    int main()
    {



    printf("hello world");
    return 0;

    }
  • 而如果使用命令

    $gcc -E define.c -o define.i -D N //-D为用命令宏定义了N,让N存在 

    则生成的预处理文件

    //最终代码
    int main()
    {



    printf("Hello World");
    return 0;

    }
    • 用命令进行宏定义,方便程序员为不同平台不同客户生成不同代码
    • 另一种宏开关
    #ifndef //if not define
    #else
    #endif
     用法:当某个头文件不存在时可以用这种办法自己解决
    
    #ifndef __STDIO_H__ //如果没有包含stdio.h
    //进行定义接口
    ...
    ...
    #endif

编译

作用:将c语言代码编译成汇编代码

两种形式

gcc -S xxx.i -o xxx.s //生成汇编文件
gcc -S xxx.c -o xxx.s

源代码

#include <stdio.h>

int main()
{
printf("Hello World");

return 0;
}

生成的汇编代码

	.file	"hello.c" 
.text
.section .rodata
.LC0:
.string "Hello World"
.text
.globl main
.type main, @function
main: #main函数起始
.LFB0:
.cfi_startproc
pushq %rbp #push入栈
#将rbp寄存器的值保存在栈帧中
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp #mov移动 移入寄存器 赋值过程
#把rsp的值移动到rbp中
#rbq register base pointer栈帧基址寄存器
#rsp register stack pointer栈顶基址寄存器
.cfi_def_cfa_register 6
leaq .LC0(%rip), %rdi#lea (load effective address 加载有效地址)
# .一般代表注释 lea加载rip的内容即lc0的内容到rdi中,rdi一般存放的是第一个参数的地址,即参数寄存器
movl $0, %eax#eax 存放返回值 目前存放了0
call printf@PLT #call函数调用 printf打印字符串
movl $0, %eax #同上
popq %rbp #pop出栈 恢复rbp的的值
.cfi_def_cfa 7, 8
ret #ret函数返回 eax存放是0,于是返回0
#以%开头的都是寄存器
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
.section .note.GNU-stack,"",@progbits

上世纪70年代最常用cpu 8086

信息

x86架构 数据总线16b,地址总线20b
一个字长word 16b
l - long word 32b
q - qwadra 64b

汇编转二进制

as命令

AS - the portable(可移植) GNU assembler(汇编).

转换命令

as hello.s -o hello.o

生成的目标文件目前不能直接执行,通过nm命令可以列出所有符号

nm hello.o
---
U _GLOBAL_OFFSET_TABLE_
0000000000000000 T main
U printf
//U代表目前还不知道地址 unknow

​ 广义的常用的生成命令(从.c文件一键生成到.o目标文件)

gcc -c hello.c -o hello.o

反编译

objdump命令

命令使用

objdump hello.o -d test.o

​ 接着就会在控制台直接显示所有内容


hello.o: 文件格式 elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # b <main+0xb>
b: b8 00 00 00 00 mov $0x0,%eax
10: e8 00 00 00 00 callq 15 <main+0x15>
15: b8 00 00 00 00 mov $0x0,%eax
1a: 5d pop %rbp
1b: c3 retq

链接

作用:把函数的名字换成地址

ld命令可以进行链接,但不推荐,可能一个函数里调用了另一个函数,这样用ld进行链接会很麻烦

简便做法

gcc命令 不加任何参数,间接调用,就可以把文件的各个函数链接起来

gcc hello.o -o hello

反汇编hello文件

0000000000201010 B __bss_start
0000000000201010 b completed.7698
w __cxa_finalize@@GLIBC_2.2.5
0000000000201000 D __data_start
0000000000201000 W data_start
0000000000000570 t deregister_tm_clones
0000000000000600 t __do_global_dtors_aux
0000000000200dc0 t __do_global_dtors_aux_fini_array_entry
0000000000201008 D __dso_handle
0000000000200dc8 d _DYNAMIC
0000000000201010 D _edata
0000000000201018 B _end
00000000000006e4 T _fini
0000000000000640 t frame_dummy
0000000000200db8 t __frame_dummy_init_array_entry
0000000000000844 r __FRAME_END__
0000000000200fb8 d _GLOBAL_OFFSET_TABLE_
w __gmon_start__
0000000000000700 r __GNU_EH_FRAME_HDR
00000000000004f0 T _init
0000000000200dc0 t __init_array_end
0000000000200db8 t __init_array_start
00000000000006f0 R _IO_stdin_used
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
00000000000006e0 T __libc_csu_fini
0000000000000670 T __libc_csu_init
U __libc_start_main@@GLIBC_2.2.5
000000000000064a T main
U printf@@GLIBC_2.2.5
00000000000005b0 t register_tm_clones
0000000000000540 T _start
0000000000201010 D __TMC_END__

发现所有函数都找到了对应的地址

执行可执行程序

只要当前用户对文件有x权限即可执行

通过./filename执行可执行程序

库文件

即公用的工具,轮子,是一种特殊的.o文件,他人写好的并且公开发行的,自己拿来用

库文件创建过程

image-20231122225617537

静态库和动态库

静态库:轮子打包到产品中,比喻:家用汽车在生成时就已经组装到车里了,一般情况下不会改变

image-20231122230038719

动态库:在运行的时候轮子才会组装起来,比喻:比赛时用的汽车,例如F4赛车,在赛场上需要跑起来时才会组装上

image-20231122230157337

对比:

  • 静态文件比较大,动态库在运行时才会组装,动态库更加小一些

  • 部署起来静态文件更加容易

  • 动态库更加容易升级,因为轮子换起来更加方便,静态库轮子不方便更换

一般情况下,gcc生成的目标文件都是调用的动态库,加上参数-static调用静态库,根据ldd命令查看文件属性可知.so为动态库文件, .a为静态库文件

生成静态库

​ 当我们调用一个只声明的函数时

#include <stdio.h>

int add(int,int);
int main()
{
printf("add(3,4) = %d\n",add(3,4));

return 0;
}

编译为test.o的目标文件是可以通过的,但是通过gcc进行链接操作时出现了问题

test.o:在函数‘main’中:
test.c:(.text+0xf):对‘add’未定义的引用
collect2: error: ld returned 1 exit status

此时我们再编译一个add.c文件

int add(int lhs,int rhs)
{
return lhs + rhs;
}

编译同样可以通过,但进行gcc链接操作也出现了问题

(.text+0x20):对‘main’未定义的引用
collect2: error: ld returned 1 exit statu
  • test.o缺少了add函数的地址,add.o缺少了main函数的地址

接下来我们手动链接test和add

gcc test.o add.o -o test

发现可以通过,且输出

add(3,4) = 7

整个过程简略图

image-20231123163047396

如何将add.o文件变为共享库

  1. 生成目标文件

    $gcc -c add.c -o add.o
  2. 将目标文件打包成静态库文件

    $ar crsv libadd.a add.o
  3. 生成了一个以libadd为前缀以.a为后缀的静态库文件

  4. 将文件移动到系统搜索目录中 (/usr/lib 切换为root用户)

    $sudo cp libadd.a /usr/lib

    检查此时是否已经复制到了当前目录中

image-20231123164133408

  1. 此时重新生成test目标文件

    gcc test.o -o test -ladd   
    你的库叫什么名字在-l后面就加什么名字且不带空格
  2. 查看是否成功

    image-20231123164646219

配置代码环境

  1. vimplus

  2. vscode

    vscode环境配置详情见vscode远程连接linux环境配置 | 涅槃豆の博客

动态库链接

  1. 先把原先的add.c和test.c复制到dynamic文件夹中,进行我们动态库链接的热身

image-20231123174535292

  1. 生成test.o目标文件

  2. 将add.c编译成目标文件

    (动态库的文件在运行时才会加载,运行时会存储在栈和堆中间的一部分区域中,称为共享库映射区)

gcc add.c -o add.o -fpic
  1. 打包成库文件

    gcc -shared add.o -o libadd.so

    lib为固定前缀,so为固定后缀

  2. 将生成的libadd.so移动到/usr/lib目录中

    image-20231123175651898

  3. 检查系统目录是否存在该文件

    image-20231123175721381

  4. 编译为可执行文件时加上-l参数(如果生成的还是静态库链接文件记得删除掉/usr/lib中的libadd.a文件)

    gcc -c test.c -o test -ladd

    通过ldd命令可以查看链接情况

    image-20231123180057168

​ 如果此时删除 libadd.so,是没办法运行的,而再把so文件放回去,又可以运行了

软链接 符号链接

软链接实际上就像快捷方式一样,里面的某个字符串存储了真实文件的路径

在动态库文件需要更新时,可以不用删除lib目录下对应的so文件,而是将so文件改造为软链接的方式,链接到新版本的动态库文件

image-20231123181138535

通过ln命令进行软链接

image-20231123181447308

此时我们作为作者更新了我们的add.c源文件

int add(int lhs,int rhs)
{
return lhs + rhs + 1;
}

再重新进行生成动态库文件的操作

$ gcc -c add.c -o add.o -fpic
$ gcc -shared add.o -o libadd.so.0.2
$ sudo cp libadd.so.0.2 /usr/lib

重新进行软链接操作

%cd /usr/lib
$sudo rm libadd.so
$sudo ln -s libadd.so.0.2 libadd.so

再次执行test可执行文件

image-20231123182323150

如果需要回滚版本只需要重新进行软链接

gcc其他选项

  • -D xxx 用命令宏定义一个xxx变量,相当于在代码内部首行添加一个#define xxx

  • -I /xxx/xxx 增加一个头文件搜索目录

  • 编译优化

    -O0 不优化

​ -O1

   -O2
 
  -O3 1->3优化越来越深 

gdb

调试

gdb和vs的调试很类似,但多了一种类似黑匣子的功能,可以记录信息

man的描述

gdb - The GNU Debugger
  • gdb使用时建议不要开优化,因为优化后会修改最终运行时的代码,调试时的汇编代码可能和源文件差别很大

    因此编译时需要加上两个参数

    gcc filename -o xxx -O0 -g    (-g是记录调试信息)

创建一个用于调试的代码

#include <stdio.h>

void func(int i)
{
printf("I'm func, i = %d\n",i);
}
int main()
{
int j = 10;
int arr[3] = {1,2,3};
int *p;
arr[2] = 4;
p = arr;
func(j);

for(int i = 0;i < 5;++i)
{
puts("hello world!");
}

return 0;
}

编译时记得加上参数

gcc test.c -o test -O0 -g
  1. 此时我们使用gdb命令即可进入调试
gdb test

image-20231124214042899

  1. 此界面使用list可以默认显示10行代码

image-20231124214227553

  1. 再输入list进入下一页

image-20231124214329799

  1. 输入list1即可回到首行

image-20231124214408654

输入l也和list效果相同

  1. 如果一个程序有多个源文件,也可以指定是哪个文件,比如要查看test.c文件的第一行

输入 l test.c:1

image-20231124214651757

  1. 同样如果要找某个函数也可以做到,比如要找到main函数

输入l test.c:main

image-20231124214955200

  1. 如果要运行程序就输入 run或者r

image-20231124215143723

  1. 打断点的方法 break或者b 同样可以指定行或者函数

image-20231124215407023

  1. 在打完断点之后,发现不是一次运行完了

    image-20231124215552111

  2. 现在我们可以选择进行单步调试,也可以选择输入continue运行到下一个断点

    image-20231124215716050

  3. 进行单步调试可以输入next或者n或者 step或者s

    image-20231124215937357

​ 此时我们发现他进入到了printf.c文件中,虽然没有找到,输入finish可以运行到printf函数运行执行完毕

image-20231124220158811 再输入next刚好跳出func函数回到main函数里面

image-20231124220304872

  1. 如果我们想在运行时删除掉某个断点,则可以用到delete命令,输入delete+空格+对应断点编号即可,而输入delete是删除所有断点

  2. info break或者ib可以查看某行的断点信息,以及相应的命中次数

    image-20231124221727038

  3. ingnore +空格+ 编号+ 次数 ,意味着忽略某号断点多少次

在gdb查看监视

  • 输入print + 对应变量名可以查看变量数据

    image-20231124222741554

  • 也可以进行对应计算

    image-20231124222841137

    • 用print显示会有些麻烦 ,而用display加变量名可以在运行时一直显示

      image-20231124224030101

​ info display可以查看监视信息,从而得知显示的变量的编号

​ 再输入undisplay + 编号即可不再显示该变量

在gdb中查看内存

​ 需要使用x命令,因为命令很复杂我们可以用help查看如何 使用

image-20231124224553569

(gdb)x/FMT ADDRESS
x/count + letter1 + letter2
  • count代表要看多少单位
  • letter1选择相应格式o,x,d,u,t,f,a,i,c,s,和printf格式输出类似
  • letter2选择单位大小b,h,w,g分别为1b,2b,4b,8b

例如我们要查看arr数组内存时

(gdb) x/3tw arr
以二进制形式查看3个单位的变量,且每个变量大小为4字节,地址是arr的地址

image-20231124225246954

  • 此时我们也可以通过内存地址查看系统是大端存储(低字节高地址)还是小端存储(低字节低地址)
(gdb)x/4tb arr

image-20231124225614144

​ 发现1存储在低地址,所以是大端存储

​ 如果感觉不明显可以修改数据然后查看

image-20231124230601336

​ 此时很明显看到高位存储在低地址,确定是大端存储

检查崩溃程序

黑匣子->core文件,存储了程序崩溃时刻内存的堆栈情况

例如我们创建一个必定崩溃的代码

#include <stdio.h>

int main()
{
int *p;
*p = 1;

return 0;
}

image-20231126224316202

编译也可以通过

运行时发现,发生段错误,也就是指针指向出错了

image-20231126225110969

此时发现目录下多了一个core文件

image-20231126230825136

  • 如果没有生成,可以按以下步骤操作

    1. 查看core文件可以创建多大

      image-20231126230518829

    2. 如果core文件大小为0,根据core文件参数,目前显示是-c

      则我们输入以下命令(该修改只是暂时的,下次重新进入系统会恢复默认)

      ulimit -c unlimited

      image-20231126230750177

      发现已经修改成功为unlimited,并再次执行代码查看目录是否有core文件

      image-20231126230853232

  • 如果还没有生成

    1. 切换管理员 su root

    2. 输入以下命令 echo core > /proc/sys/kernel/core_pattern

      image-20231126225852754

    3. 退回到当前用户 exit

      再次执行代码并查看是否有core文件

      image-20231126230934622

如果core文件生成成功,我们就可以通过gdb查看他是什么时候出错了

$gdb ./filename core

image-20231126231321023

我们再测试另一种情况,并通过gdb查看是哪里出错

void func(int i)
{
func(i + 1);
}
int main()
{
func(1);

return 0;
}

编译成功后运行

image-20231126231706285

通过gdb查看是哪里出错

image-20231126231901562

发现第三行出错,栈爆了

gdb加命令行参数

过去的c语言main函数中都有两个参数argc和argv[ ]

#include <stdio.h>

int main(int argc,char* argv[])
{
printf("argc = %d\n",argc);

for(int i = 0;i < argc;++i)
{
puts(argv[i]);
}

return 0;
}
  • 我们可以在编译运行后查看argc和argv都是什么

    image-20231126232914678

  • 当我们再运行时添加一些东西时,args和argv的值就会发生变化

    image-20231126232958102

也就是说args记录了argv数组有几个元素,argv数组记录了各个参数

  • 如果用gdb去启动

    image-20231126233124517

    使用set命令可以为其添加参数(非覆盖)

    image-20231126233212352

Makefile

makefile增量编译

如果要编译一个系统需要很长时间,且这段时间程序员因为系统cpu等硬件正在满负载工作无法做任何事情,因此需要增量编译来减少编译浪费时间

增量编译首先要维持一种”目标-依赖”关系,构成树的结构,改变某个节点只会影响他的子树

makefile的实现

  1. 文件名必须是Makefile/makefile

  2. 规则的集合:由目标文件的名称,依赖文件的名称,命令,分号作为目标和依赖的分隔符

    image-20231127145402132

  3. 把最终要生成的文件作为第一个规则

  • 创建main.c和add.c文件
#include <stdio.h>

int add(int,int);

int main()
{
printf("add(3,4) = %d\n",add(3,4));

return 0;
}
int add(int lhs,int rhs)
{
return lhs + rhs;
}
  • 在该目录下生成makefile文件并编辑(注意分号分隔符,和命令需要tab键按下后编辑)

    main:main.o add.o
    gcc main.o add.o -o main
    main.o:main.c
    gcc -c main.c -o main.o
    add.o:add.c
    gcc -c add.c -o add.o
  • 保证文件的格式都是正确之后,输入make命令,发现需要生成的文件都生成了

    image-20231127150629145

  • 当源文件比目标文件修改时间要新的话,再使用make命令就会进行增量编译

    比如我修改了add.c

    image-20231127151020144

​ 使用make命令,会发现add.o

image-20231127151051257

​ 如果要执行特定指令 可以在make后加上对应目标文件,例如 make main.o则是以main.o作为目标起点

伪目标

​ 特点:目标不存在,执行命令是不会生成目标文件的

因为目标不存在,所以导致了每次make都一定执行他的命令

​ 修改makefile文件,添加一个clean目标文件

​ 解释原理:原先的main和main.o,add.o都有对应的依赖文件,形成的树结构如下

image-20231127154632409

而clean没有依赖文件,当输入make clean以clean为起点时

image-20231127154745236

clean只会执行属于自己的命令,没有依赖文件也就是没有子树所以main,main.o,add.o的命令都不会执行

main:main.o add.o
gcc main.o add.o -o main
main.o:main.c
gcc -c main.c -o main.o
add.o:add.c
gcc -c add.c -o add.o
clean:
rm -f main.o add.o main

在make时加上clean,每次都会执行对应命令

image-20231127153747597

全量编译

可以在makefile文件中加入一些东西

main:main.o add.o
gcc main.o add.o -o main
main.o:main.c
gcc -c main.c -o main.o
add.o:add.c
gcc -c add.c -o add.o
clean:
rm -f main.o add.o main
rebuild:clean main

rebulid的依赖文件为clean和main,先依赖的clean因此会先执行clean的命令,将main.o,add.o和main都删除,然后再执行main命令,又依次生成了main.o,add.o,main

image-20231127155138209

这样的删除全部生成文件然后又重新生成的操作成为全量编译

  • 为了makefile规范,我们通常在伪目标之前声明一下

    main:main.o add.o
    gcc main.o add.o -o main
    main.o:main.c
    gcc -c main.c -o main.o
    add.o:add.c
    gcc -c add.c -o add.o
    .PHONY:clean rebuild
    clean:
    rm -f main.o add.o main
    rebuild:clean main

    .PHONY就是声明clean和rebuild是伪目标,没有任何作用

变量

makefile原内容

main:main.o add.o
gcc main.o add.o -o main
main.o:main.c
gcc -c main.c -o main.o
add.o:add.c
gcc -c add.c -o add.o
.PHONY:clean rebuild
clean:
rm -f main.o add.o main
rebuild:clean main
  1. 自定义变量: 变量名:=值 (在makefile中,变量中所有值都是字符串类型)

    引用变量: $(变量名)

    使用自定义变量后就可以更改原来的内容了

    OUT:=main #定义变量
    OBJS:= main.o add.o

    $(OUT):$(OBJS)
    gcc $(OBJS) -o main
    main.o:main.c
    gcc -c main.c -o main.o
    add.o:add.c
    gcc -c add.c -o add.o
    .PHONY:clean rebuild
    clean:
    rm -f $(OBJS) $(OUT)
    rebuild:clean main
  2. 预定义变量:预先就有值的意思

    image-20231127160351326

    根据预定义变量我们可以把gcc替换成$(CC),rm-f替换成$(RM)

    OUT:=main
    OBJS:= main.o add.o

    $(OUT):$(OBJS)
    $(CC) $(OBJS) -o main
    main.o:main.c
    $(CC) -c main.c -o main.o
    add.o:add.c
    $(CC) -c add.c -o add.o
    .PHONY:clean rebuild
    clean:
    $(RM) $(OBJS) $(OUT)
    rebuild:clean main

    使用make rebuild发现使用的是cc而不是gcc

    image-20231127160726607

    我们可以修改预定义变量的值

    CC:=gcc

    image-20231127160826075

  3. 自动变量:同一变量名根据规则变化自动变化,类似auto

    image-20231127160927455

    根据表格我们可以如此修改

    OUT:=main
    OBJS:= main.o add.o
    CC:=gcc
    $(OUT):$(OBJS)
    $(CC) $^ -o $@
    main.o:main.c
    $(CC) -c $^ -o $@
    add.o:add.c
    $(CC) -c $^ -o $@
    .PHONY:clean rebuild
    clean:
    $(RM) $(OBJS) $(OUT)
    rebuild:clean main

用%字符管理格式关系

  • 通过观察我们可以发现main.o和add.o的目标文件,依赖关系,命令格式是完全相同的,我们可以用%来合并为一个,%负责从上一个规则的依赖关系的字符串中把数据匹配出来

    我们可以如下修改

    OUT:=main
    OBJS:= main.o add.o
    CC:=gcc
    $(OUT):$(OBJS)
    $(CC) $^ -o $@
    %.o:%.c
    $(CC) -c $^ -o $@
    .PHONY:clean rebuild
    clean:
    $(RM) $(OBJS) $(OUT)
    rebuild:clean main

    %.o第一次匹配到的是main.o,此时的%代表的就是main,执行完这一个规则后再次匹配,匹配到add.o,此时%就是add,如此往复知道没有可匹配的结束这一整个大规则

  • 如果我们想要新增一个函数在main中调用

    main.c

    #include <stdio.h>

    int add(int,int);
    int sub(int,int);

    int main()
    {
    printf("add(3,4) = %d\n",add(3,4));
    printf("sub(3,4) = %d\n",sub(3,4));
    return 0;
    }

    sub.c

    int sub(int lhs,int rhs)
    {
    return lhs - rhs;
    }

    此时我们直接在makefile文件中的OBJS变量中加上sub.o即可

    执行make并运行main

    image-20231127200102670

    image-20231127200132186

​ 可以看出makefile方便了我们程序员进行编译的操作

内置函数

​ 如果觉得增量编译一个.c文件需要修改一行还是很麻烦,那么还有我们的内置函数

  • wildcard通配符

    从当前目录所有文件中取出符合要求的文件名

    因此我们可以创建一个变量,通过通配符获取当前目录所有的.c文件

    SRCS:=$(wildcard *.c)

    如果相要检查是否完全获取到,可以通过伪目标的命令来查看

    all:
    echo $(SRCS)
    OUT:=main
    SRCS:=$(wildcard *.c)
    OBJS:= main.o add.o sub.o
    CC:=gcc
    $(OUT):$(OBJS)
    $(CC) $^ -o $@
    %.o:%.c
    $(CC) -c $^ -o $@
    .PHONY:clean rebuild all
    clean:
    $(RM) $(OBJS) $(OUT)
    rebuild:clean main
    all:
    echo $(SRCS)

    通过make all命令检查

    image-20231127202053979

  • patsubst(pattern substitute)模式匹配

    类似一种函数的形式,将指定变量中.c后缀的替换成.o后缀赋值到当前命令中

    OBJS:=$(patsubst %.c,%.o,%(SRCS))

    此时即使我们的代码有调整,有新增的.c文件,makefile也不需要修改任何东西

    我们只需要使用make命令即可完成之前的一系列操作

最终版本

OUT:=main
SRCS:=$(wildcard *.c)
#OBJS:= main.o add.o sub.o
OBJS:=$(patsubst %.c,%.o,$(SRCS))
CC:=gcc
$(OUT):$(OBJS)
$(CC) $^ -o $@
%.o:%.c
$(CC) -c $^ -o $@
.PHONY:clean rebuild all
clean:
$(RM) $(OBJS) $(OUT)
rebuild:clean main
all:
echo $(SRCS)

make命令

image-20231127202834689

第二版makefile

之前的makefile最终都是只为编译一个最终的目标文件

而当我们正常使用时,一般都会想每个代码分别编译运行,让每个代码单独编译链接

image-20231127203918841

最大的区别是我们可以通过伪目标一次生成不同的目标文件,通过make all实现一次编译多个文件

SRCS:=$(wildcard *.c)
EXES:=$(patsubst %.c,%,$(SRCS))
CC:=gcc
all:$(EXES)
%:%.c
$(CC) $^ -o $@

此时我们发现3个分别输出1,2,3的文件一次编译生成了image-20231127204756602

  • 如果说我们代码的源文件在不同目录下,例如存放makefile在根目录下,源代码在根目录的src目录下,比如我们在wildcard下做如此操作即可

    SRCS:=$(wildcard src/*.c)