OpenWRT開發之——對C++的支持(解決庫依賴問題)

C++是本人的強項。如果在OpenWrt中不能用C++進行開發,那就有點大失所望了。

接下來將與大家一起來嘗試寫一個C++程序,並把它做成 ipk 包,並試運行。


各文件內容

在 SDK/package/ 路徑下創建 cpp-demo 目錄,並生成一個非常簡單的 cpp 程序

?
1
2
3
4
5
6
7
cd SDK
mkdir -p package/cpp-demo
cd package/cpp-demo
touch Makefile
mkdir src
cd src
touch Makefile main.cpp

形成如下目錄結構:

?
1
2
3
4
5
6
$ tree package/demo-cpp
package/cpp-demo/
|-- Makefile
`-- src
    |-- main.cpp
    `-- Makefile


package/cpp-demo/src/main.cpp內容:

?
1
2
3
4
5
6
7
8
9
#include <iostream>
 
using namespace std;
 
int main(int argc, char **argv)
{
    cout << "This is C++ Demo" << endl;
    return 0;
}


package/cpp-demo/src/Makefile內容:

?
1
2
3
4
5
6
7
8
9
10
11
target=cpp-demo
 
ALL:$(target)
 
objects=main.o
 
cpp-demo: $(objects)
    $(CXX) -o $(target) $(objects)
 
clean:
    @rm -rf $(objects)

注意:上面用的是CXX,而不是CC,就這點區別。


package/Makefile內容:

?
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
include $(TOPDIR)/rules.mk
 
PKG_NAME:=cpp-demo
PKG_RELEASE:=1
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
 
include $(INCLUDE_DIR)/package.mk
 
define Package/cpp-demo
    SECTION:=utils
    CATEGORY:=Utilties
    TITLE:=cpp-demo
endef
 
define Build/Prepare
    $(MKDIR) -p $(PKG_BUILD_DIR)
    $(CP) ./src/* $(PKG_BUILD_DIR)
endef
 
define Package/cpp-demo/install
    $(INSTALL_DIR) $(1)/bin
    $(INSTALL_BIN) $(PKG_BUILD_DIR)/cpp-demo $(1)/bin
endef
 
$(eval $(call BuildPackage,cpp-demo))


解決鏈接錯誤

在SDK目錄下 make V=s,結果報錯:

?
1
2
Package cpp-demo is missing dependencies for the following libraries:
libstdc++.so.6


對錯誤的初步認識

說是要依賴 libstdc++.so.6 這個庫文件?

那好吧,在本地機上安裝一個便是了。

?
1
2
3
4
sudo yum install libstdc++
...省略...
Package libstdc++-4.4.7-11.el6.i686 already installed and latest version
Nothing to do

說明本地已安排了libstdc++了的。難不成是版本對不上號?

我用 locate命令找了一下 libstdc++這個文件,發現存在 /usr/lib/libstdc++.so.6 文件。這應該就是上面需要的庫吧。

應該在LD_LIBRARY_PATH環境變量中沒有將 /usr/lib 加入到其中。如下:

?
1
$ LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/lib make V=s

這次,報:

?
1
2
3
4
Package cpp-demo is missing dependencies for the following libraries:
libc.so.6
libm.so.6
libstdc++.so.6

不湊效?!


發現依賴的是目標機的動態庫

不過靜下心來想想,我編譯cpp-demo需要的是開發機的動態庫呢,還是目標機的動態庫?

動態庫是程序在運行起來後加載的庫,那麼cpp-demo在目標機上運行起來後,加載的動態庫應該是目標機的,不應該是開發機的。所以,剛剛我在整開發機上的動態庫,與這個應該是兩碼事兒。


在SDK路徑下搜所有的動態庫文件:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
find -name lib*.so.*
./staging_dir/target-mips_34kc_uClibc-0.9.33.2/usr/lib/libzmq.so.4
./staging_dir/target-mips_34kc_uClibc-0.9.33.2/usr/lib/libevent_openssl-2.0.so.5
./staging_dir/target-mips_34kc_uClibc-0.9.33.2/usr/lib/libpopt.so.0.0.0
...<略>
./staging_dir/target-mips_34kc_uClibc-0.9.33.2/usr/lib/mysql/libmysqlclient.so.16
./staging_dir/target-mips_34kc_uClibc-0.9.33.2/usr/lib/mysql/libmysqlclient_r.so.16.0.0
...<略>
./staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/lib/libc.so.0
./staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/lib/libstdc++.so.6
...<略>
./staging_dir/host/lib/libltdl.so.7
./staging_dir/host/lib/liblzma.so.5

有很多,也看到了 libstdc++.so.6

應該是在鏈接時將 libstdc++.so.6 所在的路徑加進來吧。就像:

?
1
g++ -L./staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/lib/ -lstdc++

這樣。


排除編譯錯誤

我們檢查編譯調試信息打印出來的LDFLAGS的值:

?
1
2
3
4
LDFLAGS="-L/home/hevake_lcj/Workspace/OpenWRT/OpenWrt-SDK/staging_dir/target-mips_34kc_uClibc-0.9.33.2/usr/lib\
         -L/home/hevake_lcj/Workspace/OpenWRT/OpenWrt-SDK/staging_dir/target-mips_34kc_uClibc-0.9.33.2/lib\
         -L/home/hevake_lcj/Workspace/OpenWRT/OpenWrt-SDK/staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/usr/lib\
         -L/home/hevake_lcj/Workspace/OpenWRT/OpenWrt-SDK/staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/lib "

這說明,libstdc++.so.6已在LDFLAGS的路徑範圍之下,編譯不應該出錯。

還是好好研究一下錯誤提示:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
make[4]: Entering directory `/home/hevake_lcj/Workspace/OpenWRT/OpenWrt-SDK/build_dir/target-mips_34kc_uClibc-0.9.33.2/cpp-demo'
make[4]: Nothing to be done for `ALL'.
make[4]: Leaving directory `/home/hevake_lcj/Workspace/OpenWRT/OpenWrt-SDK/build_dir/target-mips_34kc_uClibc-0.9.33.2/cpp-demo'
touch /home/hevake_lcj/Workspace/OpenWRT/OpenWrt-SDK/build_dir/target-mips_34kc_uClibc-0.9.33.2/cpp-demo/.built
mkdir -p /home/hevake_lcj/Workspace/OpenWRT/OpenWrt-SDK/bin/ar71xx/packages /home/hevake_lcj/Workspace/OpenWRT/OpenWrt-SDK/build_dir/target-mips_34kc_uClibc-0.9.33.2/cpp-demo/ipkg-ar71xx/cpp-demo/CONTROL /home/hevake_lcj/Workspace/OpenWRT/OpenWrt-SDK/staging_dir/target-mips_34kc_uClibc-0.9.33.2/pkginfo
install -d -m0755 /home/hevake_lcj/Workspace/OpenWRT/OpenWrt-SDK/build_dir/target-mips_34kc_uClibc-0.9.33.2/cpp-demo/ipkg-ar71xx/cpp-demo/bin
install -m0755 /home/hevake_lcj/Workspace/OpenWRT/OpenWrt-SDK/build_dir/target-mips_34kc_uClibc-0.9.33.2/cpp-demo/cpp-demo /home/hevake_lcj/Workspace/OpenWRT/OpenWrt-SDK/build_dir/target-mips_34kc_uClibc-0.9.33.2/cpp-demo/ipkg-ar71xx/cpp-demo/bin
find /home/hevake_lcj/Workspace/OpenWRT/OpenWrt-SDK/build_dir/target-mips_34kc_uClibc-0.9.33.2/cpp-demo/ipkg-ar71xx/cpp-demo -name 'CVS' -o -name '.svn' -o -name '.#*' -o -name '*~'xargs -r rm -rf
Package cpp-demo is missing dependencies for the following libraries:
libc.so.6
libm.so.6
libstdc++.so.6
make[3]: *** [/home/hevake_lcj/Workspace/OpenWRT/OpenWrt-SDK/bin/ar71xx/packages/base/cpp-demo_1_ar71xx.ipk] Error 1

從前3行看來,cpp-demo的編譯好像是正常通過了的。

查看 build_dir/target-mips

?
1
2
3
4
5
6
7
8
9
10
11
12
13
tree build_dir/target-mips_34kc_uClibc-0.9.33.2/cpp-demo/
build_dir/target-mips_34kc_uClibc-0.9.33.2/cpp-demo/
|-- cpp-demo
|-- ipkg-ar71xx
|   `-- cpp-demo
|       |-- bin
|       |   `-- cpp-demo
|       `-- CONTROL
|-- main.cpp
|-- main.o
`-- Makefile
 
4 directories, 5 files

在這個路徑下已經成功生成了cpp-demo可執行程序。證明,編譯是通過了的。

爲什麼 find 執行完就報錯?


深入研究錯誤根源

怎麼入手?從錯誤提示入手吧,看看是在哪裏提示"Package cpp-demo is missing dependencies for the following libraries"的。

在SDK目錄下執行 grep 命令進行查找。

?
1
2
grep "is missing dependencies for the following libraries" ./* -R
./include/package-ipkg.mk:            echo "Package $(1) is missing dependencies for the following libraries:" >&2; \


package-ipkg.mk

打開 include/package-ipkg.mk 文件看個究竟。


在74行輸入錯誤提示信息,條件是存在文件 $(PKG_INFO_DIR)/$(1).missing 這個文件。那問題來了,這個文件是怎麼產生的呢?

行70~71行的意思是:如果在 $(PKG_INFO_DIR)/$(1).provides 中沒有找到$FILE的,就把$FILE寫入到 $(PKG_INFO_DIR)/$(1).missing
其中 $(1)值爲cpp-demo,相應的 cpp-demo.provides 與 cpp-demo.missing 都是在 SDK/staging_dir/target-mips_34kc_uClibc-0.9.33.2/pkginfo/ 路徑下的。

那麼 xxx.provides 文件裏的內容代碼什麼呢?


63~68行爲一個執行進程,它用管道與while語句連接起來。也就是說while裏面的FILE變量就是63~68行標準輸出的結果。
65,66,67是在根據平臺設置環境變量的值,然後在68行執行 SDK/scripts/gen-dependencies.sh。

我猜gen-dependencise.sh執行輸出的一定是cpp-demo所依賴的庫名稱。


研究gen-dependencise.sh

我們打開 SDK/scripts/gen-dependencise.sh 文件,其中最核心的一段:

?
1
2
3
4
5
6
7
8
9
10
#!/usr/bin/env bash
 
TARGETS=$*
XARGS="${XARGS:-xargs -r}"
 
find $TARGETS -type f -a -exec file {} \; | \
  sed -n -e 's/^\(.*\):.*ELF.*\(executable\|shared object\).*,.* stripped/\1/p' | \
  $XARGS -n1 $READELF -d | \
  awk '$2 ~ /NEEDED/ && $NF !~ /interpreter/ && $NF ~ /^\[?lib.*\.so/ { gsub(/[\[\]]/, "", $NF); print $NF }' | \
  sort -u

從TARGETS目錄下查看到所有的文件。對每個文件並用file命令輸入該文件的基本信息。用sed對file的輸出進行分析,找出有"ELF"且有"executable"或"shared object"關鍵字的文件。
再對這個文件用 readelf -d 命令輸入ELF文件的信息,再用awk從中提取出所依賴的 *.so 文件名,並打印到標準輸出。


我親手用readelf查看一下cpp-demo依賴什麼:

?
1
2
3
4
5
6
7
8
9
10
$ readelf -d build_dir/target-mips_34kc_uClibc-0.9.33.2/cpp-demo/cpp-demo 
 
Dynamic section at offset 0x934 contains 23 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [libstdc++.so.6]
 0x00000001 (NEEDED)                     Shared library: [libm.so.6]
 0x00000001 (NEEDED)                     Shared library: [libgcc_s.so.1]
 0x00000001 (NEEDED)                     Shared library: [libc.so.6]
 0x0000000c (INIT)                       0x80484e8
...<省略>

它確實依賴 libstdc++.so.6, libm.so.6, libc.so.6 這3個動態庫文件。


現在 package-ipk.mk 文件中過程理清楚了。cpp-demo所依賴的庫文件,在cpp-demo.provides 文件中找不到就會寫入到 cpp-demo.missing。

如果純粹是想讓它打包成功的話,那就把差的文件寫入到cpp-demo.provides文件就了事兒了。

這樣做會不會有後有後遺症?

試試再說~,結果還是不成功。原因是cpp-demo.provides是自動生成的。就算是我手動改了它,下次make的時候還是會還原的。


那新問題是:Who在生成這個cpp-demo.provides文件?它是根據什麼生成這個文件的?

那就grep一下關鍵字"$(1).provides",應該就可以找到。結果還是在 package-ipk.mk 文件裏:

L185,將數據進行排序了之後寫入到 cpp-demo.provides。數據由L179~184產生。

L179,將IDIR_$(1)目錄下的所有 lib*.so*, *.ko 文件名打印出來,作爲已有的庫文件,由L185寫入cpp-demo.provides文件。

IDIR_$(1)的定義如下:

L180~184這個for循環不太好理解。

其中patsubst,是個字串替換命令,其詳細用法見:[makefile中的patsubst

$(patsubst %,$(PKG_INFO_DIR)/%.provides,$(IDEPEND_$(1))) 返回的結果是:將$(IDEPEND_$(1))中的每個結果 item,變成:$(PKG_INFO_DIR)/$(item).provides返回。


那IDEPEND_$(1)是什麼?


博主深深地感到腦容量不夠了。有哪位高人知道的,請指點一下。


跳出這個洞,回到L180~184,的問題。它就是把其它的某個相關的 xxxx.provides 文件裏的內容輸出來。

博主通過經驗推斷,xxxx.provides 代表的是是與cpp-demo依賴的組件所依賴的庫。


解決方案一:在 Makefile 中添加 Package/cpp-demo/extra_provides 宏

注意:L184,是 $(Package/$(1)/extra_provides)。也就是說,我們可以在 package/cpp-demo/Makefile 文件中定義 Package/cpp-demo/extra_provides 宏來強制性地將那幾個庫加進去。比如:

?
1
2
3
4
5
define Package/cpp-demo/extra_provides
    echo "libstdc++.so.6"
    echo "libc.so.6"
    echo "libm.so.6"
endef

經過試驗,正確的寫法如下:

?
1
2
3
4
5
define Package/cpp-demo/extra_provides
    echo 'libstdc++.so.6'; \
    echo 'libm.so.6'; \
    echo 'libc.so.6';
endef

這樣寫果然湊效,再 make V=s,能夠打包成功。

但是,有點我們必須明確的是:在打包中生成的ipk文件裏,是沒有libstdc++, libc, libm這3個庫的。如果所安裝的OpenWrt系統裏也沒有這3個庫,那麼我們安裝的應用程序是不能正常使用的。
相當於是在騙ipk工具,我們已具備了上面這3個庫文件。

比較穩妥的方法是採用方案二,如下:


解決方案二:在 Makefile 中的 Package/cpp-demo/install 宏中準備所需的庫文件

還有另一個方法,注意L176,$(call Package/$(1)/install, $$(IDIR_$(1))),這個就是引用了我們在Makefile裏寫的 Package/cpp-demo/install 宏麼?
我們可以在這個宏裏,將它需要的幾個庫文件複製到 $(1) 對應的目錄下。

如下修改:

?
1
2
3
4
5
6
7
8
9
define Package/cpp-demo/install
    $(INSTALL_DIR) $(1)/bin
    $(INSTALL_BIN) $(PKG_BUILD_DIR)/cpp-demo $(1)/bin
 
    $(INSTALL_DIR) $(1)/usr/lib
    $(INSTALL_DATA) $(TOOLCHAIN_DIR)/lib/libstdc++.so.6 $(1)/usr/lib
    $(INSTALL_DATA) $(TOOLCHAIN_DIR)/lib/libm.so $(1)/usr/lib/libm.so.6
    $(INSTALL_DATA) $(TOOLCHAIN_DIR)/lib/libc.so $(1)/usr/lib/libc.so.6
endef

其中,INSTALL_DATA 與 TOOLCHAIN_DIR 這兩個變量在 rules.mk 文件中定義。
之所以選用 TOOLCHAIN_DIR,是因爲libstdc++.so.6這個文件就在這個變量所對應的路徑下。不信,你可以用 find 命令查找一下。

好了,這樣再make V=s,就能正常打包了。


解決方案三:在 Makefile 文件的 Package/cpp-demo 宏中加 DEPENDS 描述

這個方法不是我想到的,是[GunNRose]給我建議。修改 Makefile:

?
1
2
3
4
define Package/cpp-demo
    ...<略>
    DEPENDS:+=libstdcpp
endef

經博主親自嘗試,是OK的。

不過,爲什麼是叫libstdcpp而不是libstdc++呢?這個有待研究一下。

官方Dependence文檔:http://wiki.openwrt.org/doc/devel/dependencies


試用

將生成的 SDK/bin/ar71xx/packages/base/cpp-demo_1_ar71xx.ipk 文件用 scp 傳到目標機上進行安裝試用。

?
1
2
3
4
scp bin/ar71xx/packages/base/cpp-demo_1_ar71xx.ipk [email protected]:
[email protected]'s password: 
cpp-demo_1_ar71xx.ipk                    100%  278KB 278.0KB/s   00:01
$

然後用 SSH 登陸到目標機上去安裝。

?
1
2
3
root@OpenWrt:~# opkg install cpp-demo_1_ar71xx.ipk 
Installing cpp-demo (1) to root...
Configuring cpp-demo.

試執行一下 cpp-demo

?
1
2
root@OpenWrt:~# cpp-demo 
/bin/cpp-demo: line 3: syntax error: unexpected word (expecting ")")

Oops~  運行不起來。


解決運行不起來的問題

檢查一下那3個依賴的庫文件,都還在。那爲什麼不能運行起來呢?

之前博主在排查上面庫依賴的問題的時候已看出了端倪。

在 SDK/build_dir/target-mips_34kc_uClibc-0.9.33.2/cpp-demo/ipkg-ar71xx/cpp-demo/ 路徑下的所有內容都是直接打包的文件。而這裏面的 bin/cpp-demo 可執行文件在開發機上居然都正常運行。

這明顯不對,bin/cpp-demo 應該是運行在目標機上的ELF文件,怎麼可以在開發機上運行?開發機與目標機都不是同類型的CPU。一定是這裏出了問題!

博主在想,是不是在交叉編譯的時候,選用了本機的gcc,而非目標機的gcc。如下爲 SDK/package/cpp-demo/src/Makefile :

?
1
2
3
4
5
6
7
8
9
10
11
12
target=cpp-demo
 
ALL:$(target)
 
objects=main.o
 
cpp-demo: $(objects)
    echo "CXX = $(CXX)"
    $(CXX) -o $(target) $(objects)
 
clean:
    @rm -rf $(objects)

爲了知道CXX到底是哪一個編譯器。我特地加了 echo "CXX = $(CXX)" 調試信息。

把 SDK/build_dir 刪了重新 make。爲了方便分析,

?
1
2
rm -rf build_dir
make V=s | less

分析打印的信息:

?
1
2
3
4
5
6
CC="mips-openwrt-linux-uclibc-gcc" GCC="mips-openwrt-linux-uclibc-gcc" CXX="mips-openwrt-linux-uclibc-g++"
...
make[4]: Entering directory `/home/hevake_lcj/Workspace/OpenWRT/OpenWrt-SDK/build_dir/target-mips_34kc_uClibc-0.9.33.2/cpp-demo'
make[4]: Nothing to be done for `ALL'.
make[4]: Leaving directory `/home/hevake_lcj/Workspace/OpenWRT/OpenWrt-SDK/build_dir/target-mips_34kc_uClibc-0.9.33.2/cpp-demo'
...

咦?Nothing to be done for 'ALL',表示沒有什麼需要再編譯的了。

我趕緊查看

?
1
2
[SDK]$ ls build_dir/target-mips_34kc_uClibc-0.9.33.2/cpp-demo/
cpp-demo  ipkg-ar71xx  main.cpp  main.o  Makefile

發現cpp-demo已經生成,而且在開發機上可運行。這個cpp-demo是怎麼來的?難不出是從 package/cpp-demo/src裏來的?

我突然想到,我之前進入 package/cpp-demo/src/ 路徑下,執行過一次 make,那時一定生成了一個 cpp-demo 可執行程序。查看之:

?
1
2
[SDK]$ ls package/cpp-demo/src/
cpp-demo  main.cpp  main.o  Makefile

果然有它!在make時,由於它的存在,編譯組件就認爲已經有了就不必再編譯,所以才導致了上述的問題。

刪除cpp-demo以及main.o。回到SDK,重新make

?
1
2
rm -rf build_dir
make V=s | less

分析打印的信息:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
CC="mips-openwrt-linux-uclibc-gcc" GCC="mips-openwrt-linux-uclibc-gcc" CXX="mips-openwrt-linux-uclibc-g++"
...
make[4]: Entering directory `/home/hevake_lcj/Workspace/OpenWRT/OpenWrt-SDK/build_dir/target-mips_34kc_uClibc-0.9.33.2/cpp-demo'
mips-openwrt-linux-uclibc-g++ -Os -pipe -mno-branch-likely -mips32r2 -mtune=34kc -fno-caller-saves -fhonour-copts -Wno-error=unused-but-set-variable \
  -msoft-float -mips16 -minterlink-mips16  -I/home/hevake_lcj/Workspace/OpenWRT/OpenWrt-SDK/staging_dir/target-mips_34kc_uClibc-0.9.33.2/usr/include \
  -I/home/hevake_lcj/Workspace/OpenWRT/OpenWrt-SDK/staging_dir/target-mips_34kc_uClibc-0.9.33.2/include \
  -I/home/hevake_lcj/Workspace/OpenWRT/OpenWrt-SDK/staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/usr/include \
  -I/home/hevake_lcj/Workspace/OpenWRT/OpenWrt-SDK/staging_dir/toolchain-mips_34kc_gcc-4.8-linaro_uClibc-0.9.33.2/include \
  -c -o main.o main.cpp
echo "DEBUG: CXX = mips-openwrt-linux-uclibc-g++"
DEBUG: CXX = mips-openwrt-linux-uclibc-g++
mips-openwrt-linux-uclibc-g++ -o cpp-demo main.o
make[4]: Leaving directory `/home/hevake_lcj/Workspace/OpenWRT/OpenWrt-SDK/build_dir/target-mips_34kc_uClibc-0.9.33.2/cpp-demo'
...

根據調試信息可以知道cpp-demo是用mips-openwrt-linux-uclibs-g++來編譯的。這下應該就對了!

再次嘗試將ipk文件傳到目標機上並安裝。運行效果如下:

?
1
2
root@OpenWrt:~# cpp-demo 
This is C++ Demo

成功了!


總結

這一次,一個小小的cpp-demo實驗,經歷了太多波折。不過在嘗試解決這些問題的過程中,我們可以更深入地瞭解其中的編譯機制。

首先,我們知道了創建C++應用與C應用沒什麼區別,莫非就是在Makefile中將CC改成CXX。

然後我們瞭解到了在打包的時候,它是如何查找一個ELF文件依賴哪些動態包。並知道如何將所依賴的包加入到我們的ipk中。

最後,我們瞭解到了在rules.mk中定義了大量有用的變量,在include/package-ipk.mk文件中瞭解到打包的過程。

總之,這次折騰也算是蠻有收穫的。


轉自:http://my.oschina.net/hevakelcj/blog/411944



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章