一、前言
? ? ? ? 最近在項目需要將C++版本的opencv集成到原本的代碼中從而進行一些簡單的圖像處理。但是在這其中遇到了一些問題,首先就是原本的opencv我們需要在x86的架構上進行編譯然后將其集成到我們的項目中,這里我們到底應該將opencv編譯為x86架構的還是編譯成ARM架構,其次就是,編譯完成以后,我們得到的都是.so的二進制文件,我們應該怎么將其鏈接到我們自己的項目中呢?帶著這些問題,所以才有了這一篇教程。所以本次教程就是為了教大家如何將一個開源的項目交叉編譯,并且將其鏈接到自己的項目中,如果你準備好了,就讓我們開始吧!
二、誰適合本次教程
? ? ? ? 因為本次教程已經涉及到Linux C應用開發了所以并不適用小白,所以請具備一定的Linux基礎以后,再閱讀本次教程。在教程中很多關于Linux的基礎操作我并不會細講,甚至有的簡單的步驟我會直接省略。讓我們開始吧!
三、動態鏈接的概念
? ? ? ? 首先我們來講講什么是動態鏈接,以及什么是動態鏈接庫。首先來講講什么是動態鏈接,動態鏈接是一種在程序運行時解析外部函數和變量引用的機制。簡單來說,我們在編譯程序時,并不需要將這部分程序編譯到二進制文件中而是在程序運行時進行調用。然后就是動態連接庫,動態鏈接庫是包含可被多個程序共享的代碼和數據的文件。動態連接庫往往被我們編譯成二進制文件(.dll或.so)然后在外部通過.h或者其它接口進行調用,這也是目前最主流的編程方式。總的來說動態連接庫解決了代碼重復依賴的問題,我們將一部分公共的代碼編譯到一個二進制文件中并且對外開放接口,外部程序通過預留接口訪問動態連接庫從而實現一套代碼被多個程序使用。其次,如果我們的代碼涉及機密,可以將其編譯成動態鏈接庫并且開放接口給用戶使用,用戶在可以使用完整函數功能的同時又不能獲取源代碼。了解了這些,下面就來帶大家看看如何交叉編譯動態鏈接庫并且將其鏈接到自己的項目中!
四、動態鏈接庫的編譯與鏈接
? ? ? ? 這里大家需要注意,我們后面所說的編譯都是指交叉編譯,也就是說我們會在x86的設備上交叉編譯動態鏈接庫,并且將其鏈接到自己的代碼中再將其放到開發板端運行。如果你只想在x86設備上完成此操作其實也可以直接看本次教程,因為不管需不需要交叉編譯,道理都是一樣的,唯一不一樣的就是交叉編譯后的程序需要放到開發板端運行。
? ? ? ? 這里我們首先需要安裝交叉編譯環境,交叉編譯器的安裝與環境變量的添加在之前交叉調試的文章中已經講過了,大家可以直接參考:
vs code交叉調試教程:[Linux]從零開始的vs code交叉調試arm Linux程序教程-CSDN博客
這里我就默認大家已經安裝好了Ubuntu并且已經安裝好了交叉編譯器,需要像這樣能夠輸出版本號,如下圖所示:
輸入命令以后,能夠有上面的輸出就表示交叉編譯環境沒有問題,就可以進行下一步了。
這里我們同樣使用圖形化中的vs code進行操作,首先我們需要新建一個文件夾,直接使用下面的命令即可:
mkdir Project
然后我們再用vs code打開這個文件夾,這個文件夾后面也會作為我們的工程文件夾:
然后我們新建一個用于編譯動態鏈接庫的.c文件,這里我就直接叫“lib.c”了:
下面我們可以將下方的測試代碼拷貝到這個.c的文件中:
#include "stdio.h"void Hello_World()
{printf("Hello World\n");
}void Hello_Gcc()
{printf("Hello Gcc\n");
}void Hello_Arm()
{printf("Hello Arm\n");
}
因為我們使用這個.c的文件編譯動態鏈接庫,所以并不需要寫主函數。又因為是用于測試,所以寫的函數非常簡單,寫好以后,如圖所示:
大家將代碼寫入文件以后,記得保存。這里我們還需要一個.h文件來調用動態鏈接庫中的函數,因為在動態鏈接庫中只包含了函數的定義,沒有包含聲明,我們需要在.h文件中聲明這些函數,這也是為了給外部一個接口供外部調用。這里我直接新建了了一個名為“lib.h”的文件:
我們在.h文件中寫入下面的代碼聲明我們在.c文件中定義的函數:
#ifndef __LIB_H__
#define __LIB_H__void Hello_World();
void Hello_Gcc();
void Hello_Arm();#endif
寫入以后,如圖所示:
這里同樣的,寫入以后記得保存,然后我們在.c文件中引用這個.h文件:
這里我們修改完以后,我們將.c和.h文件都保存好,然后我們在項目目錄下使用下面的命令將我們剛剛的代碼編譯成動態鏈接庫:
aarch64-linux-gnu-gcc -fPIC -shared -o lib.so lib.c
這里還是來簡單解釋一下這段代碼,首先是“aarch64-linux-gnu-gcc”,這就是我們編譯時使用的交叉編譯器,這里就不多說了,然后“-fPIC”是為了生成位置無關代碼,這是動態庫的要求。然后是“-shared
”是為了告訴編譯器要將這個文件編譯成動態鏈接庫,“-o lib.so”是為了指定生成的二進制文件的名字,這里生成的二進制名字就叫“lib.so”,最后“lib.c”就是我們輸入的源文件了。
編譯完成以后,就可以看到我們的項目目錄下多了一個.so的文件,這個就是我們通過.c文件編譯出的動態鏈接庫:
這里我們可以使用“nm”工具來查看這個動態鏈接庫中是否包含了我們寫的函數,先使用下面的命令安裝一個“nm”的工具庫:
sudo apt install binutils
安裝完成以后,我們使用下面的命令來檢查我們的.so文件,這里lib.so就是我們編譯出來的動態鏈接文件:
nm -D lib.so
輸入命令以后,可以看到許多關于這個動態鏈接庫的信息,看不懂沒關系,我們只需要找輸出的信息中有沒有我們剛剛寫的函數,這里可以看到,我們寫的函數已經成功的編譯到動態鏈接庫中了:
這一步一般不會出錯,就不多說了。
下面我們來使用一下這個被我們編譯出來的動態鏈接庫,這里我們直接在原本的項目文件夾中直接新建一個名為“main.c”的文件:
下面我們在main.c中直接輸入下面的代碼:
#include "lib.h"int main()
{Hello_World();Hello_Gcc();Hello_Arm();
}
這里我們只需要引用我們動態鏈接庫的頭文件即可。
寫入完成以后,如圖所示:
然后我們使用下面的命令來編譯這個main.c文件:
aarch64-linux-gnu-gcc main.c -o main -L./ lib.so
這里還是來簡單解釋一下命令,首先就是“aarch64-linux-gnu-gcc”這就不多說了,然后是“main.c”這是我們編譯時輸入的源文件,同樣不多說了,“-o main”表示我們要輸出的二進制文件名為“main,”
這里的“-L”表示自己指定.so文件路徑,這里我寫的“./”表示在當前目錄下搜索.so文件,最后“lib.so”表示要鏈接的庫的名稱。
比那一完成以后,就可以看到項目目錄下多了一個名為“main”的可執行文件:
我們下面再使用“nm”工具來查看一下我們編譯出來的可執行文件:
這里可以看到,我們的函數已經被編譯到這個可執行文件中了,但是可以看到這些函數的前面都有一個U,這里的U表示“Undefined”,這也證實了這些函數未在我們的可執行文件中定義,需要從別的庫鏈接。
因為這個可執行文件我們是使用交叉編譯器編譯的,所以肯定是不能在X86的主機上運行的:
下面我們就來測試一下這個可執行文件是否可以正常運行,這里我們需要將可執行文件和動態鏈接庫文件發送到開發板端,這里我直接使用sftp發送,大家可以選擇自己熟悉的方式去傳輸文件:
在開發板端,我們有一個可執行文件和一個動態鏈接庫文件,如下:
這里我們需要指定一個環境變量LD_LIBRARY_PATH,這個環境變量會指定除了在標準路徑以外的路徑中尋找鏈接庫文件,我們直接使用下面的命令將這個環境變量設置為當前目錄,表示在當前目錄尋找動態鏈接庫:
export LD_LIBRARY_PATH=./
路徑設置完成以后,我們直接運行可執行文件即可:
這里我們可以看到,我們的函數可以正常打印。
如果我們不指定LD_LIBRARY_PATH路徑的話,運行可執行文件時就會提示庫找不到:
至此,我們的動態鏈接庫已經正常的編譯并且正常的鏈接到了我們的可執行文件中。
五、編譯與鏈接opencv
? ? ? ? 有了上面的經驗以后,我們就可以來實戰一下,這里就來教大家如何交叉編譯opencv庫并且鏈接到自己的項目中。
這里我們首先使用下面的命令來下載一下opencv的源碼:
wget https://github.com/opencv/opencv/archive/refs/tags/4.5.5.tar.gz
如果這里下載卡住的話,就使用下面的命令配置一下代理,大家根據自己的情況自行配置即可:
export http_proxy=http://192.168.112.10:7890
export https_proxy=http://192.168.112.10:7890
拉取到opencv的源碼壓縮包以后,如圖所示:
我們使用下面的命令解壓opencv的源碼壓縮包:
tar -xvf 4.5.5.tar.gz
解壓以后得到了下面的文件夾:
我們進入這個文件夾可以看到下面的文件夾:
下面我們準備編譯,首先在opencv項目目錄下新建一個名為“build”的目錄,并且進入:
mkdir build
然后我們在build目錄下新建一個名為build的構建文件:
touch build
下面我們在build構建文件中寫入下面的構建腳本:
cmake-DCMAKE_SYSTEM_NAME=Linux \-DCMAKE_SYSTEM_PROCESSOR=aarch64 \-DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc \-DCMAKE_CXX_COMPILER=aarch64-linux-gnu-g++ \-DCMAKE_INSTALL_PREFIX="/home/chulingxiao/Opencv/install" \-DBUILD_LIST=core,imgproc,highgui \-DBUILD_EXAMPLES=OFF \-DBUILD_TESTS=OFF \-DWITH_JPEG=ON \-DWITH_PNG=ON \..make -j4
make install
因為我們已經將交叉編譯器的可執行文件的路徑添加到環境變量了,所以這里直接寫交叉編譯器的名字即可。寫入以后我們保存退出即可。
DCMAKE_INSTALL_PREFIX變量可以設置我們編譯后安裝的路徑。這里大家自己寫安裝的路徑即可。
我們再使用下面的命令給這個構建腳本可執行權限:
chmod +x build
?然后我們使用下面的命令安裝一下cmake:
sudo apt install cmake
然后我們直接執行這個可執行文件即可:
./build
隨后就開始編譯了:
我們等待makefile生成完成即可。生成完成makefile以后編譯就開始了:
編譯完成以后,可以看到我們的文件被安裝到了如下目錄:
我們打開安裝的目錄,可以看到以下文件夾:
這里的“include”文件夾里面放了所有的頭文件:
在lib目錄下放了所有的動態鏈接庫文件:
下面來教大家如何將我們編譯出來的內容鏈接到我們自己的項目中,這里我們首先回到項目文件夾中,然后將下面的內容寫入main.c中用于測試我們opencv的功能,這是一個使用opencv將圖片二值化的程序,可以將傳入的圖片二值化:
#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{cv::Mat image = cv::imread("test.jpg", cv::IMREAD_GRAYSCALE);if (image.empty()) {std::cerr << "無法讀取圖片!" << std::endl;return -1;}// 二值化處理cv::Mat binary_image;double thresh_value = 128; // 設定閾值cv::threshold(image, binary_image, thresh_value, 255, cv::THRESH_BINARY);// 保存結果cv::imwrite("test_output.jpg", binary_image);std::cout << "二值化完成,結果保存在 test_output.jpg" << std::endl;
}
這里因為頭文件以及庫的引用比較復雜,我們寫一個makefile來幫助我們編譯文件,在項目目錄下新建一個makefile文件,將下面的內容復制到文件中:
# 編譯器設置
CXX = aarch64-linux-gnu-g++
CXXFLAGS = -std=c++11# OpenCV 路徑(改成你的安裝路徑)
OPENCV_PATH = /home/chulingxiao/Opencv/install
OPENCV_INC = /home/chulingxiao/Opencv/install/include/opencv4/
OPENCV_LIB = /home/chulingxiao/Opencv/install/lib# 程序名稱
TARGET = main
SRC = main.cOPENCV_LIBS = -lopencv_core -lopencv_imgcodecs -lopencv_imgproc $(TARGET): $(SRC)$(CXX) $(CXXFLAGS) -I$(OPENCV_INC) $< -o $@ -L$(OPENCV_LIB) $(OPENCV_LIBS)
復制以后如圖所示:
這里大家只需要修改幾個地方即可,首先就是OPENCV_PATH,這里大家將路徑改為我們一開始構建opencv時DCMAKE_INSTALL_PREFIX變量的路徑,也就是一開始設置的opencv的安裝路徑。
然后OPENCV_INC 路徑大家寫到opencv安裝路徑下的頭文件路徑,這里可以參考我寫的。
最后OPENCV_LIB 路徑大家寫到opencv安裝路徑下的動態鏈接庫路徑,這里同樣參考我寫的。
修改完以上內容以后,就沒有什么需要改了,我們直接在項目目錄下輸入“make”即可開始編譯:
這里沒有輸出別的錯誤并且生成可執行文件就表示編譯沒有問題。
下面我們將這個可執行文件通過sftp傳輸到開發板端:
然后我們在開發板端運行這個可執行文件,發現缺少庫:
我們在opencv安裝目錄中,將這個庫拷貝到開發板:
然后再在開發板端指定一下尋找庫的路徑:
export LD_LIBRARY_PATH=./
然后再次執行可執行文件,發現還缺少了一個名為“libopencv_imgcodecs.so.405”的庫:
我們再次使用sftp傳輸這個庫到開發板:
我們再次運行可執行文件,發現還缺少了一個名為“libopencv_imgproc.so.405”的庫:
我們再次使用sftp將這個庫傳輸到開發板:
我們再次運行可執行文件,發現,可執行文件已經不提示找不到庫了,提示的是找不到文件:
大家還記得我們的程序是做什么的嗎?是的,這是一個將圖像二值化的程序,要求我們傳入一個圖像,然而我們的目錄下沒有圖像,這里我們傳輸一張圖片到當前目錄下并且將名字改為“test.jpg”這也和我們程序中的名稱一樣:
我們再次執行可執行文件,可以看到,圖像已經正常被處理了,并且輸出為了“test_output.jpg”:
我們將其傳輸到可視化界面中,可以看到圖像被正常二值化:
這也證明了我們的opencv在正常工作,表示我們的交叉編譯以及so文件的鏈接都是成功的。
六、結語
? ? ? ? 盡管我們在這個過程中遇到了很多問題,但我教給大家的是解決問題的方法,這些方法也包括了如果我們在運行可執行文件缺少庫我們應該怎么辦編譯時怎樣鏈接庫不會出錯。當然,做完上面的步驟,相信大家對嵌入式Linux開發多少有一定的了解了,但這也只是學習嵌入式Linux開發的一個開始。那么最后,感謝大家的觀看!
?
?