gcc
gcc是GUN項目為C和C++提供的編譯器
入門案例
gcc編譯器最簡單的使用案例:gcc hello.c -o hello
,hello.c是源文件,-o參數指定了結果文件的名稱
gcc命令的選項:
- -v:打印編譯細節
- -E:僅僅進行預處理,把預處理結果輸出到控制臺
- -S:編譯源文件
- -c:編譯并匯編源文件
- -o:指定輸出文件
- -I <dir>:指定某個文件夾作為搜索路徑
- -L:為gcc增加一個搜索鏈接庫的目錄
工作機制
一般高級語言的編譯過程,可以分為預處理、編譯、匯編、鏈接四個階段。
預處理
預處理是編譯過程中的第一步,處理各種預處理命令,包括頭文件的包含(#include)、宏定義的擴展(#define)、條件編譯的選擇(#if #endif)等
打印出預處理的結果:gcc -E hello.c
源代碼:hello.c
#include <stdio.h>#define HELLO_LINUX "hello Linux!\n"
#define HELLO_MAC "hello MAC!\n"
#define HELLO_WINDOWS "hello Windows!\n"int main(int argc, char const *argv[]) {#if defined(__linux__)printf(HELLO_LINUX);#elif defined(__APPLE__) || defined(__MACH__)printf(HELLO_MAC)#elif defined(__WIN32__)printf(HELLO_WINDOWS);#endifint a = 1;int b = 2;#if a > bprintf("111\n");#elseprintf("222\n");#endifreturn 0;
}
打印出的結果:
# 1 "hello.c"
# 1 "<built-in>"
# 1 "<命令行>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<命令行>" 2
# 1 "hello.c"
# 1 "/usr/include/stdio.h" 1 3 4
# 27 "/usr/include/stdio.h" 3 4
# 1 "/usr/include/features.h" 1 3 4
# 375 "/usr/include/features.h" 3 4
# 1 "/usr/include/sys/cdefs.h" 1 3 4
# 392 "/usr/include/sys/cdefs.h" 3 4
# 1 "/usr/include/bits/wordsize.h" 1 3 4
# 393 "/usr/include/sys/cdefs.h" 2 3 4
# 376 "/usr/include/features.h" 2 3 4
# 399 "/usr/include/features.h" 3 4
# 1 "/usr/include/gnu/stubs.h" 1 3 4
# 10 "/usr/include/gnu/stubs.h" 3 4
# 1 "/usr/include/gnu/stubs-64.h" 1 3 4
# 11 "/usr/include/gnu/stubs.h" 2 3 4
# 400 "/usr/include/features.h" 2 3 4
# 28 "/usr/include/stdio.h" 2 3 4# 1 "/usr/lib/gcc/x86_64-redhat-linux/4.8.5/include/stddef.h" 1 3 4
# 212 "/usr/lib/gcc/x86_64-redhat-linux/4.8.5/include/stddef.h" 3 4
typedef long unsigned int size_t;
# 34 "/usr/include/stdio.h" 2 3 4# 1 "/usr/include/bits/types.h" 1 3 4
# 27 "/usr/include/bits/types.h" 3 4
# 1 "/usr/include/bits/wordsize.h" 1 3 4
# 28 "/usr/include/bits/types.h" 2 3 4typedef unsigned char __u_char;
typedef unsigned short int __u_short;
typedef unsigned int __u_int;
typedef unsigned long int __u_long;// 省略代碼int main(int argc, char const *argv[]) {printf("hello Linux!\n");int a = 1;int b = 2;printf("222\n");return 0;
}
從預處理的結果中觀察預處理究竟做了什么:
- 文件包含:#include 頭文件:會把頭文件整個復制到當前源碼文件中
- 宏展開:#define 宏名 常量:預處理會進行宏替換,把宏名提換為它所代表的常量
- 條件編譯:#if #endif:判斷應該保留哪個分支,裁剪到多余的分支
編譯
編譯:編譯器進行詞法分析、語法分析,把源代碼翻譯成匯編語言。
打印出gcc編譯器生成的匯編語言:gcc -S hello.c
,會在當前目錄下生成hello.s文件
生成的結果:
.file "hello.c".section .rodata
.LC0:.string "hello Linux!"
.LC1:.string "222".text.globl main.type main, @function
main:
.LFB0:.cfi_startprocpushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6subq $32, %rspmovl %edi, -20(%rbp)movq %rsi, -32(%rbp)movl $.LC0, %edicall putsmovl $1, -4(%rbp)movl $2, -8(%rbp)movl $.LC1, %edicall putsmovl $0, %eaxleave.cfi_def_cfa 7, 8ret.cfi_endproc
.LFE0:.size main, .-main.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)".section .note.GNU-stack,"",@progbits
匯編
匯編:把匯編代碼翻譯成機器代碼,即目標代碼
查看生成的機器代碼:
- 第一種方式:
gcc - c hello.s
,自動生成hello.o文件 - 第二種方式:
as -o hello.o hello.s
,-o參數指定生成的目標文件
查看生成的目標文件的文件類型:file hello.o,結果:
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
ELF文件
鏈接
鏈接:將符號引用和符號定義替換為可執行文件的虛擬內存地址
總結
編譯過程分為四步:預處理、編譯、匯編、鏈接
-
預處理:處理預處理命令,包括文件包含、宏定義、條件編譯
- 查看預處理的結果:gcc -E 源碼文件,會把預處理的結果打印到屏幕上
-
編譯:把預處理的結果編譯為匯編文件
- 查看編譯的結果:gcc -S 源文件,會在當前目錄下生成.s文件,.s文件中存儲了匯編代碼
-
匯編:把匯編代碼編譯為機器代碼
- 查看匯編結果:gcc -c 匯編代碼,會在當前目錄下自動生成.o文件,.o文件中存儲了機器代碼
-
鏈接:將機器代碼中的符號引用和符號定義替換為可執行文件的虛擬內存地址,最終生成可執行文件
Makefile
簡介
Makefile:一個文本文件,并且文件名就叫"Makefile"。它包含了一系列指令,定義了整個項目的編譯規則。
優點:自動化編譯,一旦寫好,只需要一個make命令,整個工程完全自動編譯,極大的提高了軟件開發的效率
make命令:解釋Makefile文件的命令
make命令的工作原則:
- 一個目標文件,只有在它依賴的目標文件被更改后,它才會被重新編譯。如果依賴文件的修改時間比目標文件的創建時間晚,證明依賴文件在目標文件創建后又進行了修改
- make工具會遍歷所有的依賴文件,并且把它們對應的目標文件進行更新。編譯的命令和這些目標文件及它們對應的依賴文件的關系則全部儲存在 Makefile 中。
入門案例
案例1
不使用Makefile的情況下對一個項目進行編譯,在這里使用到了靜態鏈接庫和動態鏈接庫
項目的源代碼:
main.c:主程序的入口
#include <stdio.h>
#include "add_minus.h"
#include "multi_div.h"int main(int argc, char const *argv[]) {printf("hello Cacu\n");int rst = 0;rst = add(3, 2);printf("3 + 2 = %d\n",rst);rst = minus(3, 2);printf("3 - 2 = %d\n",rst);rst = multi(3, 2);printf("3 * 2 = %d\n",rst);rst = div(3, 2);printf("3 / 2 = %d\n",rst);return 0;
}
add_minus.h:聲明了加法和減法的接口
int add(int, int);
int minus(int, int);
multi_div.h:聲明了乘法和除法的接口
int multi(int, int);
int div(int, int);
add_minus.c:實現了加法和減法的功能
#include "add_minus.h"int add(int x, int y) {return x + y;
}int minus(int x, int y) {return x - y;
}
multi_div.h:實現了乘法和除法的功能
#include "multi_div.h"int multi(int x, int y) {return x * y;
}int div(int x, int y) {return x / y;
}
從源代碼到可執行文件的過程:
- 編譯源文件,生成目標文件:在這里分別編譯3個".c"文件,生成".o"文件,".o"文件是目標文件,目標文件是二進制文件
- gcc -c add_minus.c
- gcc -c multi_div.c
- gcc -c main.c
- 鏈接目標文件,生成可執行文件:
gcc -o main main.o add_minus.o multi_div.o
,選項"-o"指定了目標文件的名稱,這一步會把三個".o"文件鏈接為一個可執行文件 - 清理:清理掉所有的目標文件:rm *.o
上面的案例演示了將源代碼編譯為可執行文件的基本流程,接下來還是以上面的源代碼,演示靜態鏈接庫和動態鏈接庫
- 將每個.c文件都編譯為目標文件:
gcc -c main.c; gcc -c add_minus.c; gcc -c multi_div.c
- 將add_minus.c編譯為靜態鏈接庫:
ar -r libadd_minus.a add_minus.o
,ar命令用于生成靜態鏈接庫,-r 選項指定靜態鏈接庫的位置和名稱,最好使用".o"文件來生成靜態鏈接庫,這樣和平臺是相關的。 - 將multi_div.c編譯為動態鏈接庫:
gcc multi_div.c -shared -o libmulti_div.so
,-shared表示生成動態鏈接庫,-o用于指定輸出文件 - 生成目標文件,同時連接靜態庫和動態庫:
gcc -o main main.o -L . -l multi_div -l add_minus
,-L. 表示在當前目錄下尋找庫,-l指定庫的名稱,聲明前綴lib和后綴.a、.so需要省略
案例2
最簡單的Makefile文件,還是以案例1的代碼作為源代碼
main: main.c add_minus.c multi_div.cgcc -o main main.c add_minus.c multi_div.c
在控制臺執行make命令,就會生成可執行文件main
語法
基本語法
Makefile文件的基本格式:
目標: 依賴命令 # 注意命令前必須是一個TAB鍵
make命令解析Makefile的工作機制:
- 檢測目標的依賴是否存在:生成目標前,會檢查目標的依賴文件是否存在,如果不存在,會向下檢索,檢測是否有生成依賴的規則,如果沒有,則報錯
- 檢測目標是否需要更新:如果目標的依賴有一個被更新, 則目標會被更新
在上面的入門案例2中,可執行文件依賴所有的源文件,如果一個源文件被修改,那么所有的源文件都需要被重新編譯,接下來對案例2進行優化:
main: main.o add_minus.o multi_div.ogcc -o main main.o add_minus.o multi_div.omain.o: main.cgcc -c main.cadd_minus.o: add_minus.cgcc -c add_minus.cmulti_div.o: multi_div.cgcc -c multi_div.c
在當前Makefile文件中,每個源文件都是獨立的,修改一個源文件,其它源文件不需要一同編譯。
變量和模式
Makefile中支持定義變量,使用 “變量=值” 的形式來定義變量,使用 “${變量}” 的形式來引用變量
變量的類型:普通變量、自帶變量、自動變量
普通變量:變量=值,定義完直接使用即可
自帶變量:Makefile提供的內置變量,變量名大寫
- CC:編譯器名稱,例如:CC=gcc
- CPPFLAGS: 預處理的選項
- CFLAGS:編譯器的選項
- LDFLAGS:鏈接器的選項
自動變量:只能在命令中使用,Makefile根據上下文自動賦值的變量。
在學習自動變量的含義前,首先回顧一下Makefile的基本規則:
目標: 依賴命令 # 注意命令前必須是一個TAB鍵
自動變量是對于上述格式的簡化:
- $@: 表示目標
- $<: 表示第一個依賴
- $^: 表示所有依賴, 組成一個列表, 以空格隔開, 如果這個列表中有重復的項則消除重復項。
模式:%,表示一個或多個字符,目標和依賴必須同時使用 %,表示它們有相同的名稱。比如: main.o:main.c,可以寫為 %.o: %.c
使用變量和模式來優化入門案例:
target=main
object=main.o add_minus.o multi_div.oCC=gcc
CPPFLAGS=-I ./${target}:${object}${CC} -o $@ $^%.o: %.c${CC} -o $@ -c $^ ${CPPFLAGS}
函數
makefile中的函數有很多,在這里學習兩個最常用的。
wildcard:查找指定目錄下的指定類型的文件
- 案例:
src=$(wildcard *.c)
,找到當前目錄下所有后綴為.c的文件,賦值給src,等價于src=main.c fun1.c fun2.c
patsubst:匹配替換
- 案例:
obj=$(patsubst %.c, %.o, $(src))
,把src變量里所有后綴為.c的文件替換成.o,等價于obj=main.o fun1.o fun2.o
使用函數來優化入門案例:
target=mainsrc=${wildcard ./*.c}
object=${patsubst %.c, %.o, ${src}CC=gcc
CPPFLAGS=-I ./${target}:${object}${CC} -o $@ $^%.o: %.c${CC} -o $@ -c $^ ${CPPFLAGS}
makefile清理操作
清理編譯過程中產生的中間文件,make會把makefile里出現的第一個target當作缺省target。其他的除非是生成缺省target需要,不會執行。
Makefile中的clean命令通常不會被執行,需要用戶手動執行make clean命令,來刪除編譯過程中產生的 “.o” 文件
clean命令的案例:
cleanrm -f *.o
實戰案例
案例1:
target=abooksrc=${wildcard ./*.c} # 查找當前目錄下所有的.c文件
object=${patsubst %.c, %.o, ${src}} # 將.c替換為.oCC=gcc
CPPFLAGS= -Wall -Wextra -pedantic -std=c99 # 指定編譯時的參數${target}:${object} # $@ 目標 $^ 全部參數${CC} -o $@ $^%.o: %.c${CC} -o $@ -c $^ ${CPPFLAGS}clean: # 需要執行 make clean命令,如果刪掉.o文件,make命令會重新執行rm -f *.o
在命令行執行make && make clean
命令
GDB
GDB:GNU Debugger,Linux下的調試工具,可以調試C、Java、PHP等語言。
初步使用:
- 在命令行輸入gdb,即可進入gdb提供的交互式界面
- 在交互式界面輸入help,即可查看gdb提供的幫助文檔
入門案例
程序的源代碼:gdb3.c
#include <stdio.h>int main(int argc, char const *argv[]) {int a = 0;printf("請輸入一個數字:");scanf("%d", &a);printf("%d的平方:%d\n", a, a * a);return 0;
}
編譯:gcc -g gdb3.c -o gdb3
,使用gdb調試程序,在編譯時,必須要加上-g參數。
調試:
- 進入gdb的交互式界面:
gdb
- 加載程序:
file 可執行文件
- 設置斷點:
break gdb3.c:4
- 查看源代碼:
list
- 運行程序:
run
,遇到斷點后會自動停下 - 單步執行:
step
,遇到用戶的自定義函數,會進入自定義函數。在這里,單步執行時,如果程序卡住,表示用戶需要在控制臺輸入數據 - 查看變量:
print 變量
- 繼續向下執行:
continue
,遇到斷點會停下
被調試的程序編譯時必須加入-g參數
使用gdb調試的C程序,在編譯時,必須加上 -g 參數,加上調試信息,-g選項的作用是在可執行文件中加入源代碼的信息,比如可執行文件中第幾條機器指令對應源代碼的第幾行,但并不是把整個源文件嵌入到可執行文件中,所以在調試時必須保證gdb能找到源文件。
判斷文件是否帶有調試信息:`readelf -S 可執行程序 | grep debug
交互式界面下的基本命令
運行程序:
- run(簡寫r): 運行程序,當遇到斷點后,程序會在斷點處停止運行,等待用戶輸入下一步命令
- start:啟動程序并立即停止在程序的入口處
- continue(簡寫c) : 繼續執行,到下一個斷點停止(或運行結束)
- next(簡寫n) : 單步跟蹤程序,當遇到函數調用時,也不進入此函數體;此命令同step的主要區別是,step遇到用戶自定義的函數,將步進到函數中去運行,而next則直接調用函數,不會進入到函數體內。
- step (簡寫s):單步調試如果有函數調用,則進入函數;與命令n不同,n是不進入調用的函數的
- until(簡寫u):可以運行程序直到退出循環體
- until+行號: 運行至某行,不僅僅用來跳出循環
- finish: 運行程序,直到當前函數完成返回,并打印函數返回時的堆棧地址和返回值及參數值等信息。
- call 函數(參數):調用程序中可見的函數,并傳遞“參數”,如:call gdb_test(55)
- quit(簡寫q) : 退出gdb
設置斷點:
- break 文件名:n (簡寫b n):在指定文件的第n行處設置斷點
- b fn1 if a>b:條件斷點設置
- break func(break縮寫為b):在函數func()的入口處設置斷點,如:break cb_button
- delete 斷點號n:刪除第n個斷點
- disable 斷點號n:暫停第n個斷點
- enable 斷點號n:開啟第n個斷點
- clear 行號n:清除第n行的斷點
- info b (info breakpoints) :顯示當前程序的斷點設置情況
- delete breakpoints:清除所有斷點:
查看源代碼:
- list :簡記為 l ,其作用就是列出程序的源代碼,默認每次顯示10行。
- list 行號:將顯示當前文件以“行號”為中心的前后10行代碼,如:list 12
- list 函數名:將顯示“函數名”所在函數的源代碼,如:list main
- list :不帶參數,將接著上一次 list 命令的,輸出下邊的內容。
打印表達式
- print 表達式(簡記p): 其中“表達式”可以是任何當前正在被測試程序的有效表達式,比如當前正在調試C語言的程序,那么“表達式”可以是任何C語言的有效表達式,包括數字,變量甚至是函數調用。
- print a:將顯示整數 a 的值
- print ++a:將把 a 中的值加1,并顯示出來
- print name:將顯示字符串 name 的值
- print gdb_test(22):將以整數22作為參數調用 gdb_test() 函數
- print gdb_test(a):將以變量 a 作為參數調用 gdb_test() 函數
- display 表達式:在單步運行時將非常有用,使用display命令設置一個表達式后,它將在每次單步進行指令后,緊接著輸出被設置的表達式及值。如: display a
- watch 表達式:設置一個監視點,一旦被監視的“表達式”的值改變,gdb將強行終止正在被調試的程序。如: watch a
- whatis :查詢變量或函數
- info function: 查詢函數
- info locals: 顯示當前堆棧頁的所有變量
查詢運行信息
- where/bt :當前運行的堆棧列表
- bt backtrace 顯示當前調用堆棧
- up/down 改變堆棧顯示的深度
- set args 參數:指定運行時的參數
- show args:查看設置好的參數
- info program: 來查看程序的是否在運行,進程號,被暫停的原因。
使用經驗
在上面的章節中,學習了gdb的基本知識,在這里,結合平時對于gdb的使用,總結一些常用的操作
查看函數調用棧中的棧幀
- breaktrace命令,簡寫為bt
- frame命令:簡寫為f,參數是一個數字,代表當前函數調用棧中第幾個棧幀,棧幀由下到上從1開始編碼。f 1,查看當前函數調用棧內main函數的情況
- where命令:查看當前函數調用棧
當前函數內局部變量的值
info locals
讓程序一直運行到從當前函數返回為止
finish命令
修改變量的值
set var 變量 = 值
監控某個變量的值
- display命令:監控某個變量的值,程序每運行一行,自動顯示該變量的值
- undisplay命令:取消監控
啟動程序時向程序中傳遞參數
第一種方式:在命令行中直接傳遞參數:可以在GDB命令行中使用run命令后面跟上程序的參數
gdb ./my_program
(gdb) run arg1 arg2
第二種方式:使用set args命令設置參數
gdb ./my_program
(gdb) set args arg1 arg2
(gdb) run
調試時指定跳到第幾行
jump 行號
,需要先在該行設置斷點
debug模式下無法獲取用戶輸入
在使用gdb進行調試時,gdb默認是運行在交互模式(interactive mode)下的,而交互模式會導致gdb無法正確地獲取用戶的輸入。這是因為gdb會將標準輸入重定向到自己的輸入流,而不是終端。
解決這個問題的一種方法是將gdb設置為非交互模式(non-interactive mode),這樣gdb就能夠正確地獲取用戶的輸入。可以在調試程序之前,在gdb中使用以下命令來設置非交互模式:
set pagination off
set non-stop on
這樣設置后,gdb會禁用分頁輸出(pagination)和停止在非關鍵點(non-stop mode),從而允許程序在調試過程中正常運行,而不會暫停等待輸入。
容易混淆的知識點
start和run的區別
start和run是兩個用于啟動程序的命令,它們有一些區別和不同的使用場景
- start命令:start命令用于啟動程序并立即停止在程序的入口處,然后等待調試器的進一步指令。它可以讓你在程序剛剛開始執行時暫停,以便設置斷點或進行其他調試操作。在使用start命令后,你可以使用continue命令繼續程序的執行,或者使用其他GDB調試命令進行進一步的調試操作。
- run命令:run命令用于啟動程序并從程序的起始位置開始連續執行。它會一直執行程序,直到遇到斷點、程序結束或其他調試終止條件。如果你不需要在程序剛開始執行時暫停,而是希望直接開始連續執行程序,則可以使用run命令。
常用操作
在程序執行過程中監視某個變量的改變
watch命令
執行完當前方法后返回到上一個方法
finish命令
程序中有fork函數,想要debug子進程,該怎么辦?
在程序開始執行之前,輸入命令 set follow-fork-mode child
,表示fork后將進入子進程調試,子進程調試完成,輸入命令 detach
脫離子進程,然后,set follow-fork-mode parent
,設置模式為調試父進程。
調試指定線程
info threads:列出當前程序中的線程信息
thread <thread_number>:切換到指定線程
valgrind
用于檢測內存泄漏的工具
安裝:yum install -y valgrind
使用:valgrind --leak-check=full --show-leak-kinds=all -s <程序>
,通過valgrind來執行程序,執行完成后,會顯示程序中出現的內存錯誤。-s參數用于打印出檢測到的錯誤