Linux閱碼場

文章數:550 被閱讀:871459

最新活動更多

有奖直播:Microchip适用于CryptoAuthentication™系列的可信任平台
免费下载——Maxim 面向工业应用的参考设计
【免費體驗】來這裏提交申請,有機會獲得英飛淩無人機
有獎三重奏:搶樓有禮|下單有禮|折扣狂歡繼續【英飛淩天貓旗艦店钜惠來襲下篇】
有獎活動:曬出你的電子心頭好
【双11特别推薦】新产品,新方案,#TI“芯”世界#之电机驱动器
任选下载有礼| ADI 【锂电池解决方案、参考电路合集】
RIGOL有獎直播|中國“芯”旗艦數字示波器榮耀上市

更多最新新聞

  • 第二代寶馬數字密鑰車輛訪問系統將于2019年底公布
  • Cepton将激光雷达与MechaSpin处理引擎结合 可对车辆即时分类
  • 如何使用慣性導航系統爲汽車應用提供連續的車道准確定位
  • 特斯拉新专利:电池组冷却系统 让电池组更耐用/更高效
  • 德州儀器爲降低噪聲和系統功耗推出新一代邏輯單元
  • 众泰汽车:众泰下一代车型 将搭载太阳能天窗技术
  • 元戎啓行發布L4級自動駕駛傳感解決方案,助推行業快速發展
  • 防电网过载 宝马用i3开展智能充电研究
  • TE推出具有M12連接的工業以太網交換機,符合EN50155標准
  • TE Buchanan接线端子,Heilind有售

最新精華更多

  • 1 淺析浮點數
  • 2 漫談電路、信號處理中的“虛部”
  • 3 人生苦短,我用Micropython...
  • 4 雙手撸碼20+天,串口軟件(visu...
  • 5 徒手編寫了一個STM8的反彙編工具
  • 6 2017年大学生电子竞赛F题 方案级...
  • 7 格物致知01——拍頻
  • 8 C語言char字符串與中文編碼的坑
  • 9 漫話有源濾波器——低通濾波器篇
  • 10 漫話有源濾波器——高通濾波器篇

賬號入駐

吴章金: 如何创建一个*可执行*的共享庫

2019-11-06
    閱讀數:

license: "cc-by-nc-nd-4.0"
description: "本文手把手指导如何创建一个可以执行的共享目标文件"

前言

前段时间,有多位同学在“泰晓原创团队”微信群聊到 C 语言相关的两个问题:

  • 如何讓共享庫文件也可以直接執行

  • 如何在可執行文件中用?dlopen?解析自身的函數

這兩個需求彙總起來,可以大體理解爲如何讓一個程序既可以作爲共享庫,又能夠直接運行。

这类需求在 Linux 下面其实很常见,比如 ld-linux.so 和 libc.so:

$?file?/lib/i386-linux-gnu/ld-linux.so.2
/lib/i386-linux-gnu/ld-linux.so.2:?symbolic?link?to?ld-2.23.so
$?file?/lib/i386-linux-gnu/ld-2.23.so
/lib/i386-linux-gnu/ld-2.23.so:?ELF?32-bit?LSB?shared?object,?Intel?80386,?version?1?(SYSV),?dynamically?linked
$?/lib/i386-linux-gnu/ld-2.23.so
Usage:?ld.so?[OPTION]...?EXECUTABLE-FILE?[ARGS-FOR-PROGRAM...]

$?file?/lib/i386-linux-gnu/libc.so.6
/lib/i386-linux-gnu/libc.so.6:?symbolic?link?to?libc-2.23.so
$?file?/lib/i386-linux-gnu/libc-2.23.so
/lib/i386-linux-gnu/libc-2.23.so:?ELF?32-bit?LSB?shared?object,?Intel?80386,?version?1?(GNU/Linux),?dynamically?linked

$?/lib/i386-linux-gnu/libc.so.6
GNU?C?Library?(Ubuntu?GLIBC?2.23-0ubuntu11)?stable?release?version?2.23,?by?Roland?McGrath?et?al.

那如何做到的呢?

先來看看兩類文件的區別

当前 Linux 下面的二进制程序标准格式是 ELF,这类格式可以用来表示 4 种不同类型的文件:

  • 可重定位目標文件(.o),用于靜態鏈接

  • 可執行文件格式,用于運行時創建進程映像

  • 共享目標文件(.so,共享庫),協同可執行文件創建進程映像

  • Core dump(core),运行过程中崩溃时自动生成,用于调试

我們來看中間兩類:

  • 可執行文件

    • 如果不引用外部庫函數,那麽所有符號地址是確定的,執行加載後可直接運行

  • 共享庫

    • 如果可執行文件用到外部库函数,那么需要通过动态链接器加载引用到的共享庫并在运行时解析用到的相应符号

所以,前者和後者通常情況下是獨立存在的,是聯合行動的,兩者差異明顯:

  • 可執行文件有标准的 C 语言程序执行入口?main,而共享庫则并没有这类强制要求

  • 后者为了确保可以灵活被多个可執行文件共享,所以,符号地址在链接时是相对的,在装载时动态分配和计算符号地址

接下来做个实验具体看看两者的区别,准备一个“烂大街”的 hello.c 先:

#include?<stdio.h>

int?main(void)
{
????printf("hello\n");

????return?0;
}

先来编译为可執行文件(-m32?用来生成采用 i386 指令集的代码):

$?gcc?-m32?-o?hello?hello.c
$?file?hello
hello:?ELF?32-bit?LSB?executable,?Intel?80386,?version?1?(SYSV),?dynamically?linked,?interpreter?/lib/ld-
$?./hello
hello

再來編譯爲共享目標文件,並嘗試直接執行它:

$?gcc?-m32?-shared?-fpic?-o?libhello.so?hello.c
$?file?libhello.so
libhello.so:?ELF?32-bit?LSB?shared?object,?Intel?80386,?version?1?(SYSV),?dynamically?linked
$?./libhello.so
Segmentation?fault?(core?dumped)

直接执行失败,再试试如何生成一个可執行文件来加载运行它,这个是引用共享庫的通常做法:

$?gcc?-m32?-o?hello.noc?-L./?-lhello
$?LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./?./hello.noc
hello

通过实验,可以确认“正常”创建出来的共享庫并不能够直接运行,而是需要链接到其他可執行文件中。

上述編譯選項簡介:

-shared Create a shared library.

-fpic Generate position-independent code (PIC) suitable for use in a shared library

让可執行文件可共享

接下來,好好研究一番。

先来看一个 gcc 直接支持的方式:

$?gcc?-m32?-pie?-fpie?-rdynamic?-o?libhello.so?hello.c
$?file?libhello.so
libhello.so:?ELF?32-bit?LSB?shared?object,?Intel?80386,?version?1?(SYSV),?dynamically?linked

$?./libhello.so
hello

$?gcc?-m32?-o?hello.noc?-L./?-lhello
$?LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./?./hello.noc
hello

确实可以执行,而且可以作为共享庫链接到其他可執行文件中。

上述編譯選項簡介:

-pie Produce a position independent executable on targets that support it.

-fpie These options are similar to -fpic and -fPIC, but generated position
independent code can be only linked into executables.

-rdynamic Pass the flag -export-dynamic to the ELF linker, on targets that
support it. This instructs the linker to add all symbols, not only used
ones, to the dynamic symbol table.

-rdynamic?等價于?-Wl,-E?/?-Wl,--export-dynamic,确保所有“库”中的符号都 export 到动态符号表,包括当前未用到的那些符号。

舉個例子,如果?hello.c?有一個獨立的?hello()?函數,沒有別的函數(這裏是指?main)调用到,但是其他用到该库的可執行文件希望用到它,那么?-rdynamic?就是必須的。

$?cat?hello.c
#include?<stdio.h>

void?hello(void)
{
????printf("hello...\n");
}

int?main(void)
{
????printf("hello\n");

????return?0;
}
$?cat?main.c

$?gcc?-m32?-pie?-fpie?-rdynamic?-o?libhello.so?hello.c
$?readelf?--dyn-syms?libhello.so??|?grep?hello
????19:?00000662????43?FUNC????GLOBAL?DEFAULT???14?hello

$?gcc?-m32?-o?hello.main?main.c?-L./?-lhello
$?LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./?./hello.main
hello...

如果沒有?-rdynamic,鏈接時就沒法使用。

$?gcc?-m32?-o?hello.main?main.c?-L./?-lhello
main.c:(.text+0x7):?undefined?reference?to?`hello'

同理,dlopen?自解析時也需要?-rdynamic

#include?<stdio.h>
#include?<stdlib.h>
#define?_GNU_SOURCE
#include?<dlfcn.h>

void?hello(void)
{
????printf("hello...\n");
}

int?main(void)
{
????void?*handle;
????????void?(*func)(void);
????????char?*error;

????????handle?=?dlopen(NULL,?RTLD_LAZY);
????????if?(!handle)?{
????????????fprintf(stderr,?"%s\n",?dlerror());
????????????return?EXIT_FAILURE;
????????}

????????dlerror();????/*?Clear?any?existing?error?*/

????????func?=?(void?(*)(void))?dlsym(handle,?"hello");

????????error?=?dlerror();
????????if?(error?!=?NULL)?{
????????????fprintf(stderr,?"%s\n",?error);
????????????return?EXIT_FAILURE;
????????}

????func();
????????dlclose(handle);

????return?0;
}

實測效果:

$?gcc?-m32?-pie?-fpie?-o?libhello.so?hello.c?-ldl
$?./libhello.so
./libhello.so:?undefined?symbol:?hello

$?gcc?-m32?-pie?-fpie?-rdynamic?-o?libhello.so?hello.c?-ldl
$?./libhello.so
hello...

让共享庫可执行

下面来探讨另外一种方式,在生成共享庫的基础上,来研究怎么让它可以执行。

先来回顾一下共享庫,在本文第 2 节直接执行的时候马上出段错误,基本原因是共享庫没有强制提供一个标准的 C 程序入口。

即使是我們提供了?main()(把标准 hello.c 编译为 libhello.so),程序的入口并没有指向它。

$?readelf?-h?libhello.so?|?grep?"Entry?point"
??Entry?point?address:???????????????0x3d0
$?objdump?-d?libhello.so?|?grep?3d0?|?head?-2
?380:????e8?4b?00?00?00??????????call???3d0?<__x86.get_pc_thunk.bx>
000003d0?<__x86.get_pc_thunk.bx>:

那麽,先解決入口的問題並運行,同樣出錯了:

$?gcc?-m32?-shared?-fpic?-o?libhello.so?hello.c?-Wl,-emain
$?readelf?-h?libhello.so?|?grep?"Entry?point"
??Entry?point?address:???????????????0x4b9
$?objdump?-d?libhello.so?|?grep?4b9?|?head?-2
000004b9?<main>:
?4b9:????8d?4c?24?04?????????????lea????0x4(%esp),%ecx

$?./libhello.so
Segmentation?fault

加上?-g?编译用 gdb 来看看原因:

$?gcc?-m32?-g?-shared?-fpic?-o?libhello.so?hello.c?-Wl,-emain
$?objdump?-d?libhello.so?|?grep?4b9?|?head?-2
000004b9?<main>:
?4b9:????8d?4c?24?04?????????????lea????0x4(%esp),%ecx

$
?ulimit?-c?unlimited
$?./libhello.so
Segmentation?fault?(core?dumped)

$
?gdb?./libhello.so?core
Core?was?generated?by?`./libhello.so'.
Program?terminated?with?signal?SIGSEGV,?Segmentation?fault.
#0??0x000003a6?in????()
(gdb)?bt
#0??0x000003a6?in????()
#1??0xf77344e3?in?main?()?at?hello.c:5
(gdb)?l?hello.c:5
1????#include?<stdio.h>
2
3????int?main(void)
4????{
5????????printf("hello\n");
6
7????????return?0;
8????}
(gdb)

可以看到是執行?printf?的時候出錯,說明庫函數的解析出了問題,主動用動態連接器跑一下看看:

$?/lib/i386-linux-gnu/ld-2.23.so?./libhello.so
hello
Segmentation?fault?(core?dumped)

哇哦,可以解析符號並打印了,不過最後還是崩潰了?

如果去分析 glibc 的?__libc_start_main?不難發現,我們還少調用一個標准退出函數,改造過後:

$?cat?hello.c
#include?<stdio.h>
#include?<unistd.h>

void?main(void)
{
????printf("hello\n");

????_exit(0);
}

再编译运行就没段错误了。再进一步,同样是分析 glibc,发现实际的入口函数并非?main(),而是?_start

$?cat?hello.c
#include?<stdio.h>
#include?<unistd.h>

int?main(void)
{
????printf("hello\n");

????return?0;
}

void?_start(void)
{
????int?ret;

????ret?=?main();
????_exit(ret);
}

編譯時連入口都不用指定了:

$?gcc?-m32?-g?-shared?-fpic?-o?libhello.so?hello.c
$?/lib/i386-linux-gnu/ld-2.23.so?./libhello.so
hello

也可以当共享庫使用:

$?gcc?-m32?-o?hello.noc?-L./?-lhello
$?LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./?./hello.noc
hello

最後還有一點遺憾,怎麽樣才能“動態”鏈接,而不是手動指定動態鏈接器呢?我們在程序中主動加入一個?.interp?節區來指定動態鏈接器吧。

$?cat?hello.c
#include?<stdio.h>
#include?<unistd.h>

asm(".pushsection?.interp,\"a\"\n"
????"????????.string?\"/lib/i386-linux-gnu/ld-linux.so.2\"\n"
????".popsection");

int?main(void)
{
????printf("hello\n");

????return?0;
}

void?_start(void)
{
????int?ret;

????ret?=?main();
????_exit(ret);
}

再試試,完美運行:

$?gcc?-m32?-shared?-fpic?-o?libhello.so?hello.c
$?./libhello.so
hello

最後,稍後整理一下:

$?cat?hello.c
#include?<stdio.h>

#ifdef?EXEC_SHARED
#include?<unistd.h>

asm(".pushsection?.interp,\"a\"\n"
????"????????.string?\"/lib/i386-linux-gnu/ld-linux.so.2\"\n"
????".popsection");

int?entry(void)
{
????printf("%s?%d:?%s():?the?real?entry?of?shared?library?here.\n",?__FILE__,?__LINE__,?__func__);

????/*?do?whatever?*/

????return?0;
}

int?main(void)
{
????return?entry();

????return?0;
}

void?_start(void)
{
????int?ret;

????ret?=?main();
????_exit(ret);
}
#endif

void?hello(void)
{
????printf("hello...\n");
}

当普通共享庫使用,默认编译即可,要能够执行的话,实现一下?entry(),編譯時打開?EXEC_SHARED?即可:

$?gcc?-m32?-shared?-fpic?-o?libhello.so?hello.c?-DEXEC_SHARED
$?./libhello.so
hello.c?12:?entry():?the?real?entry?of?shared?library?here.

$?LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./?./hello.main
hello...

小結

本文详细讲解了如何像 libc.so 和 ld-linux.so 一样,既可以当共享庫使用,还能直接执行,并且讲述了两种方法。

兩種方法都可以達成目標,第一種方法用起來簡單方便,第二種方法揭示了很多背後的工作邏輯。

如果想与本文作者更深入地探讨共享庫、程序执行、动态链接、内联汇编等本文涉及的内容以及它们背后的程序链接、装载和运行原理,欢迎订阅吴老师的 10 小时视频课程:报名:《360° 剖析 Linux ELF》在线视频课程已经全面上线

(完)




Linux閱碼場原创精华文章汇总

更多精彩,尽在"Linux閱碼場",扫描下方二维码关注



你的隨手轉發或點個在看是對我們最大的支持!


About Us 關于我們 客戶服務 聯系方式 器件索引 網站地圖 最新更新 手機版

站點相關: TI培訓

北京市海澱區知春路23號集成電路設計園量子銀座1305 電話:(010)82350740 郵編:100191

電子工程世界版權所有 京ICP證060456號 京ICP備10001474號 電信業務審批[2006]字第258號函 京公海網安備110108001534 Copyright ? 2005-2018 EEWORLD.com.cn, Inc. All rights reserved