從編譯連結到cmake

凪风sama發表於2024-11-05

.c(.cpp)檔案到可執行檔案

對於一份簡單的.c/.cpp為字尾的原始檔,他所使用的語言是人類可以閱讀並看懂的,但是對於計算機來說,其可理解並執行的是二進位制的機器碼。

也就是說,計算機所能執行的是二進位制的機器碼,而早期為了方便人類閱讀,使用一些簡單的助記符來代替機器碼,比如MOV,LOOP...等,而為了進一步增加程式設計的可讀性,便誕生了c語言以及後續的一眾高階語言,如c++,python等。

#include <stdio.h>
int main()
{
    printf("Hello, world!\n");
    return 0;
} // 人類語言,計算機僅可執行01010101....重複的機器碼

因此要想讓我們寫出的c或者cpp程式碼變為計算機可以執行的二進位制機器碼,就需要使用一些程式來透過一些手段將原始碼編譯成可執行的二進位制機器碼。

一般來說,編譯器的工作流程如下:

  • 預處理: 原始碼中的宏進行一切的宏展開,宏替換,以及條件編譯等操作,此操作後檔案中無任何宏定義。(.i檔案)
  • 編譯:將預處理後的檔案透過編譯器的詞法分析,語法分析,語義分析,最佳化,程式碼生成等操作,生成對應的組合語言檔案(.s檔案)
  • 彙編:將組合語言檔案透過彙編器的彙編操作,生成對應的機器碼檔案(.o檔案)
  • 連結:將多個.o檔案進行連結,生成最終的可執行檔案(.exe檔案/linux下無字尾)

因此,編譯器所做的工作就是透過自己的一套規則將原始碼轉換為機器碼。規則的制定一般是有對應的語言標準來規定,而編譯器可以透過不同的實現來實現該標準即可。編譯器更像是一個翻譯軟體,而它的工作就是根據人類的翻譯需求來將一種語言翻譯成另一種語言。常見的編譯器有gcc(開源),clang(jetbrain系列),msvc(微軟)等,不同的編譯器支援的最新標準也不同。

至於編譯器是怎麼來的,可以上網搜一搜。

本次培訓使用的編譯器是gcc,可以使用gcc --version命令檢視當前gcc版本。
若沒有安裝gcc,可以使用sudo apt-get install build-essential命令安裝gcc。

下面我們以gcc為例,來看一下如何將.c/.cpp檔案一步一步編譯成可執行檔案。

#include <stdio.h>
#define first 1
int main()
{
    printf("Hello, world!\n");
    int num = first;
    printf("%d", num);
    return 0;
} // 假設hello.c

預處理

在終端中輸入gcc -E hello.c -o hello.i指令,-E選項表示只進行預處理,-o選項指定輸出檔名為hello.i
然後開啟hello.i檔案,可以看到經過預處理後的原始碼如下:

# 0 "hello.c"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
.....//此處省略一堆東西
int main()
{
    printf("Hello, world!\n");
    int num = 1;
    printf("%d", num);
    return 0;
}

如果回到.c檔案中,我們檢視stdio這個標頭檔案,可以發現其中的內容和hello.i檔案前面的內容是一樣的,無非是標頭檔案的宏直接變成了標頭檔案所在的路徑。而且我們定義的宏first也被替換成了1

編譯

在終端中輸入gcc -S hello.i指令,-S選項表示將檔案進行編譯,輸出檔案為翻譯為彙編的.s檔案。
然後開啟hello.s檔案,可以看到經過編譯後的原始碼如下:

	.file	"hello.c"
	.text
	.section	.rodata
.LC0:
	.string	"hello world!"
.LC1:
	.string	"%d"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$16, %rsp
	leaq	.LC0(%rip), %rax
	movq	%rax, %rdi
	movl	$0, %eax
	call	printf@PLT
	movl	$1, -4(%rbp)
    ........;省略一堆

可以看出,c語言透過編譯器變為了可讀性更差的組合語言,但是還有一些東西是人可以看懂的,比如movcall等指令。

彙編

在終端中輸入gcc -c hello.s 指令,-c選項表示將檔案進行彙編,輸出檔案為二進位制檔案hello.o
image

可以看到確實變成二進位制檔案了,而且由於沒有對應的解碼器,無法直接看到其中內容。

連結

二進位制檔案連結

所謂的連結,就是將多個.o檔案進行連結,生成最終的可執行檔案。對於連結,針對的是多檔案專案的編譯,因為一個可執行檔案中可能包含多個原始檔,而這些原始檔可能包含不同的函式,因此需要將這些原始檔中的函式進行連結,生成最終的可執行檔案。

對於上述的hello.o檔案,由於是單檔案,因此可以簡單的使用
gcc hello.o -o hello生成可執行檔案hello然後執行

./hello
Hello, world!

對於多檔案來說並非如此,比如我們有兩個個檔案:

int sub(int a, int b)
{
    return a - b;
} // sub.c
#include <stdio.h>
int sub(int a, int b); // 宣告sub函式
int main()
{
    int a = 10, b = 5;
    int result = sub(a, b);
    printf("%d", result);
} // main.c

如果直接使用gcc -c main.c生成可執行檔案,回報出如下錯誤:

main.c:(.text+0x25): undefined reference to `sub'

即找不到sub函式的定義,這是顯然的,sub函式的實現在sub.c中,main.c中我們只是告訴了編譯器有一個函式sub,這個行為在預處理->編譯->彙編的過程中不會報出任何錯誤,但是在連結過程中,編譯器無法在參與連結的檔案中找到sub的實現,因此報出了連結錯誤。

因此要想不報錯,可以這樣:
gcc main.c sub.c -o main,這樣編譯器會將兩個檔案編譯為二進位制檔案,並將sub.o參與到連結過程中,生成最終的可執行檔案main。要注意,連結的是二進位制檔案,而不是原始檔。

對於使用多個二進位制檔案參與連結生成可執行檔案的方法,其好處就是,將專案拆分為多個模組,當要對專案的某一部分進行修改時,只需要重新編譯該模組為新的.o檔案即可,而不需要重新編譯整個專案。

庫檔案連結

庫檔案是指一些預先編譯好的二進位制檔案,可以被多個程式共用。分為動態庫(linux下的.so檔案/windows下的.dll檔案)和靜態庫(linux下的.a檔案/windows下的.lib檔案)之分。它們的區別就是靜態庫是連線時全部連結進可執行檔案,而動態庫是執行時才連結進可執行檔案。這裡就不演示使用gcc生成和連結庫檔案的過程了,可以參考相關資料。

至此,我們就走遍瞭如何從原始碼到可執行檔案的整個流程。

cmake

可以看到,對於比較大的專案,使用gcc的命令來進行編譯會很繁瑣和麻煩,就像由機器碼到彙編再到c語言一樣,為了簡化編譯流程,便有了從gcc -> makefile -> CMakeLists這樣的變化。

簡單介紹makefile

Makefile 是用於管理專案構建過程的工具,廣泛用於 C/C++ 等語言的編譯。它透過定義規則和指令,自動化編譯、連結等步驟,大大簡化了開發者的工作。也就是說,makefile透過一些規則來告訴編譯器如何編譯原始碼,如何連結庫檔案,如何生成可執行檔案。透過編寫規則即可完成編譯,而不需要手動指定每個編譯選項。(我也不會makefile,這裡簡單介紹一下)當編寫完makefile後,只需要執行make命令,makefile就會自動按照規則編譯生成對應的可執行檔案。注意make命令後接的是makefile的路徑。

cmake與CMakeLists.txt

對於一些複雜的專案,makefile的規則編寫起來會比較麻煩,因此出現了cmake。cmake是一種跨平臺的編譯工具,可以用來管理專案的構建,透過編寫cmake,可以自動生成對應的makefile,然後執行make命令編譯生成可執行檔案。

cmake的配置檔案是CMakeLists.txt,它是cmake的核心配置檔案,主要用於定義專案的原始檔、標頭檔案、庫檔案等資訊,以及編譯選項等。

最簡單的CMakeLists.txt如下:

cmake_minimum_required(VERSION 3.6)
# 指定專案所需要的cmake最小版本 
project(hello)
# 指定專案名稱
add_executable(hello hello.cpp)
# 新增可執行檔案hello,並指定原始檔為hello.cpp

結束後,在終端中執行cmake.命令,cmake會自動生成對應的makefile,然後執行make命令編譯生成可執行檔案。但是在生成的時候會出現很多配置檔案,因此習慣上建立一個名字為build的資料夾,然後在該資料夾下執行cmake..命令,生成的makefile就在build資料夾中,然後執行make命令編譯生成可執行檔案。

後面關於cmake的使用,轉如下連結

cmake的簡單使用

安裝opencv庫並使用

所謂的庫,即為我們提供了標頭檔案以及一些預編譯好的二進位制檔案,我們可以透過安裝庫檔案來使用一些功能。

編譯安裝opencv

這裡我們採用下載原始碼編譯安裝的方式來安裝opencv。所謂的安裝,其實就是將別人寫好的程式碼編譯為庫,然後將標頭檔案和編譯好的庫檔案複製到系統的指定目錄,同時還附帶著一些說明檔案,方便編譯時找到標頭檔案和庫檔案。

原始碼下載連結[https://github.com/opencv/opencv/releases]

安裝教程參考:
[https://blog.csdn.net/weixin_44796670/article/details/115900538]

使用opencv

參考cmake的簡單使用後半部分
這裡給出一個簡單的demo測試是否安裝成功:

#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;

int main()
{
    Mat src = Mat::zeros(500, 500, CV_8UC3); // 建立一個500x500的空白影像
    circle(src, Point(250, 250), 50, Scalar(255, 255, 255), -1); // 在影像中畫一個白色的圓
    imshow("src", src); // 顯示影像
    waitKey(0); // 等待按鍵
}

執行結果應該為黑色影像上出現了一個白色的圓。
image

相關文章