gcc的基本使用
编译gcc文件
在hello_world.c
文件中有如下代码:
#include <stdio.h>
int main() {
int i;
printf("Hello world!\n");
}
在终端中输入:
gcc hello_world.c
这样hello_world.c
文件所在的文件夹中编译出了叫做a.out
的可执行程序,但是如果我们不想让编译出来的可执行程序叫做a.out
,我们可以通过-o
选项来指定编译出来的可执行文件的名字:
gcc hello_world.c -o hello_world
这样我们编译出来得到的可执行文件名就是hello_world
,我们可以通过./hello_world
来执行它。
让gcc输出警告
在默认情况下gcc会忽略一些低等级的警告,但这些警告往往能帮助我们找到程序中的问题。通过在编译的时候添加-Wall
就可以让gcc输出全部的警告信息,按实际情况对程序进行修改。
gcc -Wall hello_world.c -o hello_world
这时程序会报warning:
hello_world.c: In function ‘main’:
hello_world.c:6:9: warning: unused variable ‘i’ [-Wunused-variable]
int i;
^
这个警告是说,我们声明了变量i
但是却没有使用它。
类似的这种警告是非常好用的,程序出现问题后我们可以根据这种提示顺藤摸瓜地找到问题的根源。
程序优化
在gcc中有一个-O
选项,后面会跟一个字符,例如-O2
,-Os
,-O0
,-Og
等代表不同的优化等级。不同的优化等级有不同的侧重点。对于-O0
,-O1
,-O2
,-O3
这四种优化大家可以这样认为,在大部分情况下,数字越大优化等级越高,程序运行效率越高,但是编译出来的文件大小也会越大,可以理解为用空间换时间。
-O2
优化
-O2
优化非常好的平衡了程序的性能与程序最后的大小;
gcc -Wall -O2 hello_world.c -o hello_world
-Os
优化
-Os
优化与-O2
相似,它只是把一些通过增大程序大小从而提高程序运行速度的优化手段给禁用了;
gcc -Wall -Os hello_world.c -o hello_world
-Og
优化
通常在在开启优化之后,源码中的一些代码可能被编译器优化掉了,那么这些代码在最终的程序中并不存在。但有时程序就是在开启优化后才出问题,而不开优化程序运行的非常正常,那么这时候我们想使用一些工具来对程序进行debug,但是源代码与最终的程序却对应不起来,这就给debug造成极大的困扰。
使用-Og
优化就可以解决这个问题,这会告诉编译器仅仅优化那些不会导致源码中的代码被优化掉的内容从而为debug提供便利。
gcc -Wall -g -Og hello_world.c -o hello_world
-O0
优化
-O0
优化的优化程度比-Og
还要低,它几乎不对代码进行任何的优化,这也是编译器默认的优化等级,就是说如果没有标注用什么优化方式的话,编译器会默认使用这个等级的优化。
gcc -Wall -g -O0 hello_world.c -o hello_world
优化带来了哪些变化?
我们在a.c
文件中写下如下代码:
#include <stdio.h>
const int N = 100000;
int main() {
int sum = 0;
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
sum += j;
}
}
printf("%d", sum);
return 0;
}
不开优化编译运行代码
(base) chant@mouxiangyus-MacBook-Pro ~ % gcc a.c -o a
(base) chant@mouxiangyus-MacBook-Pro ~ % time ./a
-1614091392./a 6.49s user 0.01s system 99% cpu 6.514 total
以我的电脑为例,会看到没开任何优化的情况下,该程序一共运行了 6.514s
开启-O2 优化编译运行代码
(base) chant@mouxiangyus-MacBook-Pro ~ % gcc -O2 a.c -o a
(base) chant@mouxiangyus-MacBook-Pro ~ % time ./a
-1614091392./a 0.00s user 0.00s system 58% cpu 0.006 total
会看到,还是在我的电脑上运行,开启-O2 优化之后,程序运行了 0.00s,
也就是说它用了非常短的时间就运行了这个程序了。对比开启优化前后,可以说是编译器对第二份代码进行了巨幅优化。
Gcov——代码覆盖率分析工具
工具的作用
这个工具可以让我们知道源码中每一行代码、每一个分支都执行了多少次。这个功能应用的场景主要是软件测试和代码优化。
软件测试
什么是软件测试?
所谓代码测试就是,我们写了一个函数,它有一定的功能,我们想象这个函数的各个应用场景,从而根据这些场景设计一些数据测试这个函数在各个场景下能否正常运行并返回我们想要的值。
这个时候不得不提软件测试中的一个笑话:
一个测试工程师走进一家酒吧,要了一杯啤酒
一个测试工程师走进一家酒吧,要了一杯咖啡
一个测试工程师走进一家酒吧,要了0.7杯啤酒
一个测试工程师走进一家酒吧,要了-1杯啤酒
一个测试工程师走进一家酒吧,要了2^32杯啤酒
一个测试工程师走进一家酒吧,要了一杯洗脚水
一个测试工程师走进一家酒吧,要了一杯蜥蜴
一个测试工程师走进一家酒吧,要了一份asdfQwer@24dg!&*(@
一个测试工程师走进一家酒吧,什么也没要
一个测试工程师走进一家酒吧,又走出去又从窗户进来又从后门出去从下水道钻进来
一个测试工程师走进一家酒吧,又走出去又进来又出去又进来又出去,最后在外面把老板打了一顿
一个测试工程师走进一
一个测试工程师走进一家酒吧,要了一杯烫烫烫的锟斤拷
一个测试工程师走进一家酒吧,要了NaN杯Null
1T测试工程师冲进一家酒吧,要了500T啤酒咖啡洗脚水野猫狼牙棒奶茶
1T测试工程师把酒吧拆了
一个测试工程师化装成老板走进一家酒吧,要了500杯啤酒并且不付钱
一万个测试工程师在酒吧门外呼啸而过
一个测试工程师走进一家酒吧,要了一杯啤酒';DROP TABLE 酒吧
测试工程师们满意地离开了酒吧。然后一名顾客点了一份炒饭,酒吧炸了
那么Gcov工具在软件测试中有什么用处呢?
我们写一个简单的功能可能在一个函数中就能实现, 那么软件测试就只需要对这个函数直接测试就可以。但在大多数情况下一个函数的功能并不是一个简单的函数就能实现,他需要把多个小功能组合成一个大功能从而实现抽象,这时候就可以使用这个工具,他可以很直观的让我们看到当前这组测试数据是否覆盖了全部的代码,如果没有,那么有哪些代码没有被覆盖,这样就可以针对这些没有被覆盖的代码再设计出一组数据从而进行测试。
代码优化
我们可以通过gcov工具查看代码中哪些代码被执行的次数非常多,这样对这些代码进行优化可以提高程序运行的效率。
Gcov的使用
如果要使用 gcov 工具,那么在使用 gcc 工具对代码进行编译的时候需要加上一下两个选项-fprofile-arcs 以及-ftest-coverage,这两个选项会分别在命令执行结束后产生 gcon 文件以及在程序运行后产生 gcda 文件。
现在在test.c
文件下写下一下代码:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int sum = 0;
int f1() { sum += 1; }
int f2() { sum += 2; }
int main() {
srand(time(0));
for (int i = 0; i < 1000; i++) {
if (rand() & 1) f1();
else f2();
}
printf("%d", sum);
return 0;
}
这段代码的意思是说我们循环1000次,每次随机一个数字,如果这个数字是奇数那么就调用函数f1
否则如果是偶数就调用函数f2
。
执行上面说到的命令:
gcc -fprofile-arcs -ftest-coverage test.c -o test
可以使用ls
命令查看当前目录下的内容:
(base) chant@mouxiangyus-MacBook-Pro test % gcc -fprofile-arcs -ftest-coverage test.c -o test
(base) chant@mouxiangyus-MacBook-Pro test % ls
test test.c test.gcno
会发现多了一个test.gcno
文件,之后运行test
,再使用ls
命令:
(base) chant@mouxiangyus-MacBook-Pro test % ./test
1492%
(base) chant@mouxiangyus-MacBook-Pro test % ls
test test.c test.gcda test.gcno
会发现又多了一个文件test.gcda
,这样以上准备工作就完毕了。
使用如下指令使用 gcov 工具:
gcov test.c
这样就在当前目录下生成了一个 test.c.gcov 文件,这里存储的就是代码运行次数的信息。
使用vim或者cat等就可以看到每行代码被执行的次数:
-: 0:Source:test.c
-: 0:Graph:test.gcno
-: 0:Data:test.gcda
-: 0:Runs:1
-: 1:#include <stdio.h>
-: 2:#include <stdlib.h>
-: 3:#include <time.h>
-: 4:
-: 5:int sum = 0;
-: 6:
508: 7:int f1() { sum += 1; }
-: 8:
492: 9:int f2() { sum += 2; }
-: 10:
1: 11:int main() {
1: 12: srand(time(0));
508: 13: for (int i = 0; i < 1000; i++) {
1000: 14: if (rand() & 1) f1();
#####: 15: else f2();
-: 16: }
1: 17: printf("%d", sum);
1: 18: return 0;
-: 19:}
最前面的数字就是每行代码被执行的次数,-
表示为空代码不执行,#####代表代码没有被执行。
需要注意的是这个代码执行次数是会累加的,也就是说如果你没有删除gcda
文件就重新运行了程序并用了gcov
命令,那么每一行代码的执行次数是这两次程序运行时该行代码运行次数的累加,而不是单独的第二次运行时的次数:
(base) chant@mouxiangyus-MacBook-Pro test % ./test
1490% (base) chant@mouxiangyus-MacBook-Pro test % gcov test.c
File 'test.c'
Lines executed:88.89% of 9
Creating 'test.c.gcov'
(base) chant@mouxiangyus-MacBook-Pro test % cat test.c.gcov
-: 0:Source:test.c
-: 0:Graph:test.gcno
-: 0:Data:test.gcda
-: 0:Runs:3
-: 1:#include <stdio.h>
-: 2:#include <stdlib.h>
-: 3:#include <time.h>
-: 4:
-: 5:int sum = 0;
-: 6:
1525: 7:int f1() { sum += 1; }
-: 8:
1475: 9:int f2() { sum += 2; }
-: 10:
3: 11:int main() {
3: 12: srand(time(0));
1525: 13: for (int i = 0; i < 1000; i++) {
3000: 14: if (rand() & 1) f1();
#####: 15: else f2();
-: 16: }
3: 17: printf("%d", sum);
3: 18: return 0;
-: 19:}
(base) chant@mouxiangyus-MacBook-Pro test % ./test
1508%
(base) chant@mouxiangyus-MacBook-Pro test % gcov test.c
File 'test.c'
Lines executed:88.89% of 9
Creating 'test.c.gcov'
(base) chant@mouxiangyus-MacBook-Pro test % cat test.c.gcov
-: 0:Source:test.c
-: 0:Graph:test.gcno
-: 0:Data:test.gcda
-: 0:Runs:4
-: 1:#include <stdio.h>
-: 2:#include <stdlib.h>
-: 3:#include <time.h>
-: 4:
-: 5:int sum = 0;
-: 6:
2017: 7:int f1() { sum += 1; }
-: 8:
1983: 9:int f2() { sum += 2; }
-: 10:
4: 11:int main() {
4: 12: srand(time(0));
2017: 13: for (int i = 0; i < 1000; i++) {
4000: 14: if (rand() & 1) f1();
#####: 15: else f2();
-: 16: }
4: 17: printf("%d", sum);
4: 18: return 0;
-: 19:}
会发现第二次执行上面一套命令,if语句那里被执行了2017次,原因就是该代码运行次数做了累加。
抽象
这里我非常想和大家分享一下在计算机科学中“抽象”这一概念,我感觉这个概念将计算机中的优雅和优美体现的淋漓尽致!!!!
这里借用维基百科上对抽象的定义:
在计算机科学中,抽象化是将数据与程序,以它的语义来呈现出它的外观,但是隐藏起它的实现细节。
这个定义非常的死板,这里可以给大家举一个例子:
这个例子是关于Hello world的。我们知道晶体管,对晶体管进行抽象得到了门电路;对门电路抽象实现了CPU并得到了机器语言;对机器语言进行抽象得到了汇编语言;对汇编语言抽象得到了高级语言;对高级语言进行抽象得到了一些常用的函数,当然在实际中它抽象的层数要比我刚刚说的要多得多。那么现在我们打下printf("Hello world!\n");
这一行代码,运行并在终端中显示出Hello world!
,支持这一个简单的操作进行的是从晶体管到高级语言语言了一整套抽象。
那这里美体现在哪里呢?我们打印出这一行字可能需要调用几万个晶体管,几千个门电路,几百条机器语言,几十条汇编语言最终抽象成这简简单单的一个函数,大家可以想象一下这个过程,这是一个非常壮丽的过程!
这种例子在计算机科学中非常多,再比如说TCP/IP协议,它有四层模型,最底层是数据链路层,之后每一层都是对上一层的抽象得到的;再比如说mysql数据库执行sql语句,其实mysql数据库是支持并发执行sql语句的,但是它通过各种抽象最终抽象出一个execute
函数,我们可以不断的去执行sql语句而不必去理会它在底层是如何实现并发的。
通过汇编了解编译器的优化
我们在test.c
文件中写下以下代码:
int main() {
int i = 0;
int j = 0;
for (i = 0; i < 5; i++) {
j = i + 1;
}
return 0;
}
观察上面的代码会发现,我们这段代码计算了一个值,最终这个值被存储在变量 j 中。但是,由于在计算到计算结束的过程中没有外部的函数使用该变量,也没有将值赋给其他变量供其他地方使用,所以四舍五入我们可以认为这段代码和下面这段代码是等价的:
int main() {
return 0;
}
因为第一段代码,我们虽然计算了值,但是我们从来没有使用过他,所以相当于什么都没干。
但是编译器最终编译出来的代码是什么样子的呢?
我们可以通过-S选项让编译器生成汇编代码:
gcc -O0 -S test.c
上面我打开了最低等级的编译器优化。
ls
会发现在当前目录下生成了一个test.S
的文件,我们可以通过vim打开它,这里我们重点关注的是从 _main: 到 LCFI2: 这之间的代码,这段代码就是我们写的 for 循环的代码,因此这里只截取了这一部分(不同编译器版本可能会产生不同的汇编代码,但是在结构上应该是大致相同的):
_main:
LFB0:
pushq %rbp
LCFI0:
movq %rsp, %rbp
LCFI1:
movl $0, -4(%rbp) # -4(%rbp)为变量i在内存中的存储地址,这里表示 i=0
movl $0, -8(%rbp) # -8(%rbp)为变量j在内存中的存储地址,这里表示 j=0
movl $0, -4(%rbp)
jmp L2 # 跳转到标签L2
L3:
movl -4(%rbp), %eax # 将i的值赋给累加器eax
addl $1, %eax # 给累加器eax + 1
movl %eax, -8(%rbp) # 将累加器的值赋给变量j, 上面三行相当于t = i; t = t + 1; j = t;
addl $1, -4(%rbp) # 循环体结束,给变量i + 1
L2:
cmpl $4, -4(%rbp) # 比较变量i与4的大小关系
jle L3 # 如果 i <= 4 则跳转到标签L3
movl $0, %eax # 给累加器清零
popq %rbp
LCFI2:
ret
这里我给代码加了一点注释。仔细阅读这段代码会发现,虽然这段代码没有任何的作用,但是编译器还是完全按照我们写的代码逐步翻译成汇编语言代码。
我们可以试着开编译器的-O2 优化,看一下编译器会如何处理这段完全不起作用的代码:
_main:
LFB0:
xorl %eax, %eax
ret
LFE0:
.section __TEXT,__eh_frame,coalesced,no_toc+strip_static_syms+live_support
这里只截取了一部分代码,我们会神奇的发现,那段关于 for 循环的代码已经被编译器优化掉的!!!
用同样的方法再测试一下下面这段代码:
#include <stdio.h>
const int N = 1000000000;
int main() {
long long sum = 0;
int i = 0;
for (; i < N; i++) {
sum += i;
}
printf("%lld\n", sum);
return 0;
}
这里只截取汇编代码中的核心部分:
_main:
LFB1:
pushq %rbp
LCFI0:
movq %rsp, %rbp
LCFI1:
subq $16, %rsp
movq $0, -8(%rbp)
movl $0, -12(%rbp)
jmp L2
L3:
movl -12(%rbp), %eax
cltq
addq %rax, -8(%rbp)
addl $1, -12(%rbp)
L2:
movl $1000000000, %eax
cmpl %eax, -12(%rbp)
jl L3
movq -8(%rbp), %rax
movq %rax, %rsi
leaq lC0(%rip), %rax
movq %rax, %rdi
movl $0, %eax
call _printf
movl $0, %eax
leave
这段汇编代码与上一个例子的汇编代码非常相似,这段代码将for循环足足跑了\(10^{9}\)次,如果大家在自己的电脑上把这段代码编译运行出来,他应该需要跑个几秒钟才能出来,在我的电脑他的运行时间:
(base) chant@mouxiangyus-MacBook-Pro test % time ./test
499999999500000000
./test 2.66s user 0.00s system 99% cpu 2.664 total
这次开启-O2
优化之后,我们再看看编译器会对这段代码又怎样的行为:
_main:
LFB1:
movabsq $499999999500000000, %rsi
subq $8, %rsp
LCFI0:
xorl %eax, %eax
leaq lC0(%rip), %rdi
call _printf
xorl %eax, %eax
addq $8, %rsp
LCFI1:
ret
!!!!编译器直接把这段for循环优化掉了,取而代之的是直接用高斯求和得到结果赋值给变量!
这样的例子有很多,很多我们感觉写的没有什么逻辑的循环、运算,编译器早就已经看透了,随手就给优化掉了。~_~
查看gcc版本
当查阅gcc的官方文档的时候,我们必须知道我们用的gcc是哪一个版本才能正确的查看,因为不同版本的gcc可能会有不同的特性。
gcc -v
在我的电脑中,执行命令后输出以下内容(删减了一部分):
gcc version 8.4.1 20200928 (Red Hat 8.4.1-1) (GCC)
那么就知道我电脑上的gcc版本为8.4.1
,那么就可以去官网上查找关于该版本的文档。