【Linux】一些工具的简单使用,vim/gcc/gdb/make
慕雪年华

本篇博客将介绍linux下面一些简单工具的使用

[TOC]

1.vim编辑器

1.1安装vim

1
sudo apt-get install vim

需要注意的是,vim编辑器下不能使用CTRL+S来保存文件,因为在linux中这个快捷键的作用是暂停该终端,整个系统都会卡住,这时候使用CTRL+Q取消暂停就可以了。

1.2文本操作

以下是命令模式下的一些文本批量化操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
yy 复制当前行,nyy复制n行
p 粘贴再当前行的后面,np粘贴n次剪贴板的内容
dd 剪切(删除)当前行,ndd操作n行
u 撤销
ctrl+r 重做
shift+g 光标快速定位到文本末尾
gg 光标快速移动到文本头
n+shift+g 光标定位到文本的第n行
shift+4 光标定位到该行末尾
shift+6 光标定位到该行开头
w,b 以单词为单位进行移动光标
h,j,k,l 左、下、上、右
shift+` 大小写快速切换
r 替换光标所在处的字符,支持nr
shift+r 批量化替换
x 删除光标所在处的字符,nx删除n个

vim进入插入模式的快捷键有a i o,分别对应不同的功能

1.3底行模式的操作

vim编辑器中底行模式的一些操作如下。在其他模式下按esc即退出到底行模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
:w   "只保存
:q "不保存退出
:wq "保存并退出
:reg "打开vim的寄存器面板
:syntax on "开启语法高亮
:set nu "显示行号
:set nonu "取消行号显示
:set tabstop=4 "设置tab的缩进,默认为8
:set softtabstop=4 "softtabstop是“逢8空格进1制表符”,前提是你tabstop=8
:set shiftwidth=4 "设置程序自动缩进所使用的空格长度
:set autoindent "自动对齐上一行(这个选项会导致复制的时候代码排版混乱,可以考虑关闭,或者开启粘贴模式)
:set paste "开启粘贴模式
:set mouse=a "设置鼠标模式,默认是a
:%s/A/B/g 将当前文件中的A全部替换成B

上面的一些配置,写入.vimrc配置文件即可长时生效。

如果需要写入.vimrc配置文件,需要先把:注释都去掉

2.gcc/g++编译器

g++操作和gcc是一样的,这里我们使用gcc作为演示

2.1linux下使用不同命令执行程序的几个阶段

第一步是预处理,只做文本操作

1
gcc -E test.c -o tset.i

在这个阶段会

  • 展开头文件
  • 对define等等操作进行替换
  • 处理条件编译指令
  • 同时删除所有注释

编译操作

1
gcc -S test.i -o test.s

汇编操作

1
gcc -c test.s -o test.o

形成可执行程序

1
gcc test.o -o mytest

这三个命令的顺序就是ESc其中只有-c选项是小写的

正好就是键盘左上角esc按键的顺序

2.2代码和库

这里我们操作/编译的都是自己的代码。比如printf我是调用的c语言库中的函数,并没有自己完成一个打印的实现。

这时候就需要和系统的c语言库产生关联

1
2
c标准库的位置
ls /lib64/libc*

上面的最后一步形成可执行程序mytest时,系统会自动帮我们把这里的代码和库里面的方法连接起来,形成一个最终的可执行程序,并使用./mytest来执行输出结果

所以我们平时说的装环境就是需要安装语言的静态和动态库,这样才能正常利用库里面的函数进行代码的编译处理

同时我们在编译器里面写代码时的代码补全功能也是通过在库函数的头文件里面搜索来完成的。


2.3动态/静态链接&库

  • 动态:linux(.so) windows(.dll)
  • 静态:linux(.a) windows(.lib)

网吧是全校所有同学共享的,你在网吧开的机子是和别人一起用的。
从学校去网吧(库),然后获得一台机子(库函数),打游戏(执行方法).
这就是一个动态的编译链接的过程,即为动态库

如果学校允许带电脑,当你想打游戏的时候用的是自己的电脑,用的是自己的方法,这种情况就是用的静态库。每一个人拥有自己的电脑,这个电脑的功能和网吧里面的功能是一样的,当我们把库中的相关代码直接拷贝到自己的可执行程序中,即为静态链接

  • 动态链接:所有人共享同一个资源
    • 优点:可以节省资源;
    • 缺点:一旦库丢失,会导致所有程序失效
  • 静态链接:都用的是自己的方法,将库里面的代码拷贝到自己的文件中
    • 优点:不依赖任何库,程序可以独立运行
    • 缺点:浪费资源

查看链接状态,默认是动态链接

1
2
3
4
5
6
[muxue@bt-7274:~/GIT/raspi/code/TestProgram]$ ldd mytest
linux-vdso.so.1 => (0x00007ffc0dd8b000)
/$LIB/libonion.so => /lib64/libonion.so (0x00007f89bd66c000)
libc.so.6 => /lib64/libc.so.6 (0x00007f89bd185000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007f89bcf81000)
/lib64/ld-linux-x86-64.so.2 (0x00007f89bd553000)

查看可执行程序的构成

1
2
[muxue@bt-7274:~/GIT/raspi/code/TestProgram]$ file mytest
mytest: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=7cf0ffacfdaeadf8de9b4c0fea379b2a15c37e4c, not stripped

2.3.1手动指定进行静态链接

1
gcc test.c -o mytest1 -static

静态链接生成的可执行程序大小很大,动态链接的默认是8040左右的体积。

1
2
-rwxrwxr-x 1 muxue muxue   8416 Aug  7 07:34 mytest
-rwxrwxr-x 1 muxue muxue 861288 Aug 7 07:44 hello

所以一般情况下我们都推荐使用动态链接,避免占用太大的空间


当我首次尝试这种方式的时候,出现了下面的报错

1
2
3
[muxue@bt-7274:~/GIT/raspi/code/TestProgram]$ gcc hello.c -o hello -static
/usr/bin/ld: cannot find -lc
collect2: error: ld returned 1 exit status

因为系统里面默认不会带.a的静态库,所以会报错。这时候需要我们手动安装一下。

1
2
sudo yum install -y glibc-static
sudo yum install -y libstdc++-static

安装成功!

1
2
3
Installed:
glibc-static.x86_64 0:2.17-326.el7_9
Complete!

这时候执行就不会报错了!

1
2
[muxue@bt-7274:~/GIT/raspi/code/TestProgram]$ gcc hello.c -o hello -static
[muxue@bt-7274:~/GIT/raspi/code/TestProgram]$

3.gdb调试

默认生成的可执行程序是无法调试的!在linux里面发布的可执行程序默认是release版本的,无法debug

需要添加一个-g选项进行编译

1
gcc test.c -o test_g -g

同时debug版本的可执行文件也会比release版本大一些,这大的空间里面存放的就是调试信息

1
2
-rwxrwxr-x 1 muxue muxue 8360 Aug  7 07:50 test
-rwxrwxr-x 1 muxue muxue 9376 Aug 7 07:53 test_g

利用下面这个语句可以查看可执行程序的调试信息

1
readelf -S test | grep debug

可以看到debug版本包含了很多调试信息,而release版本里面没有

1
2
3
4
5
6
7
[muxue@bt-7274:~/GIT/raspi/vim/TestGdb]$ readelf -S test | grep debug
[muxue@bt-7274:~/GIT/raspi/vim/TestGdb]$ readelf -S test_g | grep debug
[27] .debug_aranges PROGBITS 0000000000000000 00001061
[28] .debug_info PROGBITS 0000000000000000 00001091
[29] .debug_abbrev PROGBITS 0000000000000000 00001122
[30] .debug_line PROGBITS 0000000000000000 00001164
[31] .debug_str PROGBITS 0000000000000000 0000119f

3.1尝试调试一个简单的代码

以下是一些简单的gdb操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
b 行号:打断点
info b:查看断点
d 断点编号: 取消断点
l 行号:显示代码
l main:显示包含main的那一行
r:run,开始运行程序,跳到第一个断点
s:step,逐语句,对应vs的F11(进入函数)
n:next,逐过程,对应vs的F10
c:continue,跳转道下一个断点
p:查看变量
display / undisplay:常显示 或 取消常显示
until 行号:跳转到指定行
finish:执行完一个函数后停下
bt:查看函数调用堆栈

提醒:编译的时候记得加上-g选项指定debug版本

下面是一个用于演式的示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int Add(int a,int b)
{
printf("Add(a,b)\n");
return a+b;
}

int main()
{
printf("hello wolrd!\n");
int ret=Add(1,20);
printf("ret: %d\n",ret);
return 0;
}

演示如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
[muxue@bt-7274:~/GIT/raspi/vim/TestGdb]$ gdb test_g
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/muxue/GIT/raspi/vim/TestGdb/test_g...done.
(gdb) l
2
3 int Add(int a,int b)
4 {
5 printf("Add(a,b)\n");
6 return a+b;
7 }
8
9 int main()
10 {
11 printf("hello wolrd!\n");
(gdb) b 11
Breakpoint 1 at 0x4005a7: file test.c, line 11.
(gdb) ll
Undefined command: "ll". Try "help".
(gdb) l 10
5 printf("Add(a,b)\n");
6 return a+b;
7 }
8
9 int main()
10 {
11 printf("hello wolrd!\n");
12 int ret=Add(1,20);
13 printf("ret: %d\n",ret);
14 return 0;
(gdb) b 13
Breakpoint 2 at 0x4005c3: file test.c, line 13.
(gdb) r
Starting program: /home/muxue/GIT/raspi/vim/TestGdb/test_g

Breakpoint 1, main () at test.c:11
11 printf("hello wolrd!\n");
Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.x86_64
(gdb) s
hello wolrd!
12 int ret=Add(1,20);
(gdb) s
Add (a=1, b=20) at test.c:5
5 printf("Add(a,b)\n");
(gdb) p ret
No symbol "ret" in current context.
(gdb) finish
Run till exit from #0 Add (a=1, b=20) at test.c:5
Add(a,b)
0x00000000004005c0 in main () at test.c:12
12 int ret=Add(1,20);
Value returned is $1 = 21
(gdb) s

Breakpoint 2, main () at test.c:13
13 printf("ret: %d\n",ret);
(gdb) p ret
$2 = 21
(gdb) p &ret
$3 = (int *) 0x7fffffffdf3c
(gdb) s
ret: 21
14 return 0;
(gdb) s
15 }(gdb) q
A debugging session is active.

Inferior 1 [process 5932] will be killed.

Quit anyway? (y or n) y
[muxue@bt-7274:~/GIT/raspi/vim/TestGdb]$

整体的操作并不是特别复杂,大家可以自己尝试一番,有问题可以评论提出


4.make/makefile

这是一个批量处理工具,我们可以通过make来批量编译一些代码,避免手动敲打命令行的出错问题。这在大型项目中非常重要。

makefile是当前路径下的一个普通文件,存放了如下内容:

  • 依赖关系
  • 依赖方法

假设我们需要形成一个c语言的可执行文件

1
2
依赖关系:test -> test.c
依赖方法:gcc test.c -o test

其对应的makefile如下

1
2
test:test.c
gcc test.c -o mytest

注意,第二行的依赖方法必须tab缩进,不然无法正常调用!

编写好makefile后,直接在当前路径下执行make。系统会自动查找名称为makefile/Makefile的文件执行

1
2
3
4
5
6
7
8
9
10
[muxue@bt-7274:~/GIT/raspi/vim/TestMake]$ ls
makefile test.c
[muxue@bt-7274:~/GIT/raspi/vim/TestMake]$ make
gcc test.c -o test
[muxue@bt-7274:~/GIT/raspi/vim/TestMake]$ ls
makefile test test.c
[muxue@bt-7274:~/GIT/raspi/vim/TestMake]$ ./test
hello wolrd!
Add(a,b)
ret: 21

我们还可以写一个清除指令,用于在编译后删除大量临时出现的可执行程序

1
2
3
.PHONY:clean
clean:
rm -f test

在原本的makefile后追加这部分内容即可

通过make clean来清理文件

1
2
3
4
5
6
[muxue@bt-7274:~/GIT/raspi/vim/TestMake]$ ls
makefile test test.c
[muxue@bt-7274:~/GIT/raspi/vim/TestMake]$ make clean
rm -f test
[muxue@bt-7274:~/GIT/raspi/vim/TestMake]$ ls
makefile test.c

4.1出现missing separator解决方案

当我执行make clean的时候出现了这个报错

1
2
[muxue@bt-7274:~/GIT/raspi/vim/TestMake]$ make clean
makefile:4: *** missing separator. Stop.

这是因为在我的makefile中,依赖方法前面的缩进是4个空格,而不是1个tab

注意需要使用tab进行缩进,而不能手动打空格!

4.2make如何判断需不需要重新生成?

当我们在一个文件夹内执行过make之后,再次make,系统会提示当前的可执行程序test已经是最新版本,无需更新。

1
2
3
4
5
6
7
8
9
[muxue@bt-7274:~/GIT/raspi/vim/TestMake]$ ls
makefile test.c
[muxue@bt-7274:~/GIT/raspi/vim/TestMake]$ make
gcc test.c -o test
[muxue@bt-7274:~/GIT/raspi/vim/TestMake]$ ls
makefile test test.c
[muxue@bt-7274:~/GIT/raspi/vim/TestMake]$ make
make: `test' is up to date.
[muxue@bt-7274:~/GIT/raspi/vim/TestMake]$

那么系统是如何实别出来我们的原代码是否有过更改的呢?

4.2.1 stat时间戳

我们可以使用stat命令查看一个文件的时间戳

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[muxue@bt-7274:~/GIT/raspi/vim/TestMake]$ stat test
File: ‘test’
Size: 8440 Blocks: 24 IO Block: 4096 regular file
Device: fd01h/64769d Inode: 1450818 Links: 1
Access: (0775/-rwxrwxr-x) Uid: ( 1001/ muxue) Gid: ( 1001/ muxue)
Access: 2022-08-07 17:18:40.463120772 +0800
Modify: 2022-08-07 17:18:40.463120772 +0800
Change: 2022-08-07 17:18:40.463120772 +0800
Birth: -
[muxue@bt-7274:~/GIT/raspi/vim/TestMake]$ stat test.c
File: ‘test.c’
Size: 201 Blocks: 8 IO Block: 4096 regular file
Device: fd01h/64769d Inode: 1450788 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1001/ muxue) Gid: ( 1001/ muxue)
Access: 2022-08-07 09:30:39.043992599 +0800
Modify: 2022-08-07 09:30:38.544992699 +0800
Change: 2022-08-07 09:30:38.544992699 +0800
Birth: -

这里可以看到,一个文件的时间戳分为3个,分别是Access查看、modify修改,Change更改

第一个查看很好理解,那么modify和change有什么区别呢?

我们可以手动修改一个程序看看情况

1
2
3
4
5
6
7
8
9
10
[muxue@bt-7274:~/GIT/raspi/vim/TestMake]$ vim test.c
[muxue@bt-7274:~/GIT/raspi/vim/TestMake]$ stat test.c
File: ‘test.c’
Size: 207 Blocks: 8 IO Block: 4096 regular file
Device: fd01h/64769d Inode: 1450788 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1001/ muxue) Gid: ( 1001/ muxue)
Access: 2022-08-07 17:25:12.958082845 +0800
Modify: 2022-08-07 17:25:12.808082859 +0800
Change: 2022-08-07 17:25:12.808082859 +0800
Birth: -

这里我通过vim进入该文件,添加了一行注释,可以看到,相比于之前的时间,3个时间戳都被修改成了最新的时间。这是因为我们修改文件的时候一定会查看,也有modify和change

而如果我只是修改这个文件的权限,并不修改它的内容,会发生什么?

1
2
3
4
5
6
7
8
9
10
[muxue@bt-7274:~/GIT/raspi/vim/TestMake]$ chmod o-r test.c
[muxue@bt-7274:~/GIT/raspi/vim/TestMake]$ stat test.c
File: ‘test.c’
Size: 207 Blocks: 8 IO Block: 4096 regular file
Device: fd01h/64769d Inode: 1450788 Links: 1
Access: (0660/-rw-rw----) Uid: ( 1001/ muxue) Gid: ( 1001/ muxue)
Access: 2022-08-07 17:25:12.958082845 +0800
Modify: 2022-08-07 17:25:12.808082859 +0800
Change: 2022-08-07 17:27:34.378068762 +0800
Birth: -

可以看到,只有change发生了变化。

文件=内容+属性,在这里的modify对应的就是内容修改,而change对应的是属性修改。而当我们修改文件内容的时候,会引起文件大小的变化,也是属性变化,所以修改内容也可能会引起change的变化!


了解了这个时间戳,那么系统是怎么判断是否需要重新生成就很简单了:比较依赖关系中左边的目标文件和右边源文件的modify时间,如果源文件的modify时间早于目标文件,那么说明目标文件生成之后,源文件并没有发生更改,那么也无需再次生成

4.2.2 PHONY关键字的作用

在前面提到的clean代码中,我们使用了.PHONY关键字来修饰clean。

这个关键字让clean作为一个伪目标,且总是被执行

  • 怎么理解这个总是被执行?

当我们的源文件没有发生更改的时候,make不会重新生成,这个叫做总是不被执行

  • PHONY关键字的作用就是屏蔽系统对于modify时间的检查,每一次都会强制执行该语句的依赖方法。

一般情况下我们只有在clean的时候才会使用.PHONY关键字来修饰

4.3 $@ $^ 高级用法

1
2
mytest:test1.c test2.c
gcc -o $@ $^

第一个$@代表是左边的目标文件,$^代表是源文件,这样写可以实现全匹配,不需要自己一个一个写源文件了

1
2
3
4
5
6
7
[muxue@bt-7274:~/git/linux/code/22-08-07_make_gdb]$ cat makefile
test:test.c add.c
gcc $^ -o $@
[muxue@bt-7274:~/git/linux/code/22-08-07_make_gdb]$ make
gcc test.c add.c -o test
[muxue@bt-7274:~/git/linux/code/22-08-07_make_gdb]$ ./test
4

4.4 定义变量

image

4.5 命令行提供变量

makefile除了可以帮我们快速编译项目,有的时候我们还可以把一些常用的命令给写进去。比如打包压缩某一个目录的文件(备份)

1
tar -zcvf ./.bak/code.tar.gz ./code

上面这个命令的作用是,将code目录打包生成一个code.tar.gz压缩文件,放入到当前路径的.bak文件夹中。

此时我的需求是给这个命令传一个参数日期,让生成的压缩包文件名能带上日期,makefile可以这么写

1
2
3
.PHONY:tar
tar:
tar -zcvf ./.bak/code${d}.tar.gz ./code

这里留下了d作为一个参数,可以在执行make命令的时候指定

1
make d=230128

这样操作了之后,打包压缩出来的文件名就会是code230128.tar.gz 完美达成目的!


5.尝试编写一个简单的linux进图条

当我们在linux系统上下载一些软件的时候,总是可以看到用文字组成的进度条,这些进图条是怎么做出来的呢?

下面我们可以尝试用C语言写出一个简单的进度条。在这之前,我们需要了解一些概念

5.1 缓冲区

在我之前的C语言文件操作博客中,提到了一个缓冲区的概念。简单来说,当我们printf一道字符串的时候,系统是先把这个字符串写入缓冲区,再把缓冲区的内容输出到屏幕上

比如下面这个代码,再linux环境中,\n会自动刷新缓冲区。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <unistd.h>
int main()
{
while (1)
{
printf("hehe\n");
//在linux环境中,不带'\n'的时候,并不会打印(没有刷新缓存区)
//而在VS环境中,带不带都会正常打印
sleep(1);//linux环境中,sleep函数的参数,单位是秒(VS是毫秒)
// linux环境下,sleep函数需要小写,VS下是Sleep
}
return 0;
}

如果我们去掉\n,系统则不会立即打印内容。

image

这时候需要我们手动用fflush(stdout)刷新一下缓冲区,现在程序会在一行中打印了

1
fflush(stdout);//手动刷新缓冲区

image

5.2 回车和换行

在我们日常生活中提到的换行一般指的是回车+换行

实际上,回车和换行是有区别的:

  • 回车:光标回到该行的最前面
  • 换行:光标去到下一行,但是位置不变

在C语言中,\n执行的就是回车+换行,而\r是回车

那么我们就可以利用这个特性,来实现一个简单的倒计时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <unistd.h>
#include <stdio.h>
int main()
{
int i=9;
while (i>=0)
{
printf("%d\r",i);
//在linux环境中,不带'\n'的时候,并不会打印(没有刷新缓存区)
//而在VS环境中,带不带都会正常打印
fflush(stdout);//手动刷新缓冲区
sleep(1);//linux环境中,sleep函数的参数,单位是秒(VS是毫秒)
// linux环境下,sleep函数需要小写,VS下是Sleep
i--;
}
return 0;
}

image


5.3 进度条

做好前面的准备工作后,现在我们就可以来打印一个简单的进度条了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#define NUM 102
#define STYLE '#'

void process()
{
char bar[NUM];
memset(bar, '\0', sizeof(bar));

const char *lable = "|/-\\";//在末尾打印一个转动的“小圆圈”

int cnt = 0;
while(cnt <= 100)
{
//默认是右对齐,使用-改位左对齐
printf("加载中:%-100s[%d\%][%c]\r", bar, cnt, lable[cnt%4]);
fflush(stdout);
bar[cnt++] = STYLE;//打印预定义的符号
usleep(200000);
}
printf("\n");
}

int main()
{
process();

return 0;
}

image

这样一个简单的进图条就搞定辣!

后记

linux中一些工具的使用可能不会有windows的编译器那么方便,比如GDB调试。但是在后续编写一些只有linux平台才能运行的代码的时候,我们必须学会使用这些工具,否则操作起来会非常麻烦!

感谢你看到最后,有任何问题都欢迎在评论区提出哦!