一個奇怪的鏈接問題

原文:一個奇怪的鏈接問題


前言


鏈接是代碼生成可執行文件中一個非常重要的過程。我們在使用一些庫函數時,有時候需要鏈接庫,有時候又不需要,這是爲什麼呢?瞭解一些鏈接的基本過程,能夠幫助我們在編譯時解決一些疑難問題。比如,下面就有一種奇怪的現象。


一個奇怪的鏈接問題


程序功能很簡單,計算e的n次方。程序清單如下(代碼一):

#include<stdio.h>
#include<math.h>
int main(int argc,char *argv[])
{
    double a = exp(2);
    printf("%lf\n",a);
    return 0;
}

編譯運行:

gcc -o expTest expTest.c
./expTest
7.389056

一切似乎順理成章,我們再來看下面這種情況,將變量b=2傳入exp函數(代碼二):

#include<stdio.h>
#include<math.h>
int main(int argc,char *argv[])
{
    int b = 2;
    double a = exp(b);
    printf("%lf\n",a);
    return 0;
}

編譯:

gcc -o expTest expTest.c
/tmp/ccx5lXbS.o:在函數‘main’中:
expTest.c:(.text+0x20):對‘exp’未定義的引用
collect2: error: ld returned 1 exit status

我們發現,同樣的編譯方法編譯不過了,提示對‘exp’未定義的引用,並且拋出鏈接出錯
我們通過man命令查看exp函數:

man 3 exp
NAME
       exp, expf, expl - base-e exponential function

SYNOPSIS
       #include <math.h>

       double exp(double x);
       float expf(float x);
       long double expl(long double x);

       Link with -lm

發現它除了需要包含頭文件math.h外,編譯時還需要使用-lm鏈接。
再次編譯運行:

gcc -lm -o  expTest expTest.c 
/tmp/ccYT3E65.o:在函數‘main’中:
expTest.c:(.text+0x20):對‘exp’未定義的引用
collect2: error: ld returned 1 exit status

爲什麼還是不行呢?我們已經按照幫助手冊的只是加了-lm了啊?難道是位置不對?我們換個位置試試:

gcc -o  expTest expTest.c -lm 
./expTest
7.389056	

現在終於成功編譯並運行。


分析


雖然最後終於成功編譯運行,但是不免產生了幾個疑問:

兩段代碼同樣都調用了exp函數,爲什麼一個需要鏈接,一個不需要鏈接呢?
到底什麼時候需要鏈接呢?
爲什麼鏈接的時候放在前面就不行呢?


我們一一解答


1.爲什麼一個需要鏈接,一個不需要?
我們可以觀察到,代碼一調用exp傳入的參數是常量2,代碼二調用exp傳入的參數是變量b,那麼對於代碼一會不會在運行之前就計算好了呢?
我們來看一下它們的彙編代碼。
代碼一:

.LC1:
        .string "%lf\n"
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 32
        mov     DWORD PTR [rbp-20], edi
        mov     QWORD PTR [rbp-32], rsi
        movsd   xmm0, QWORD PTR .LC0[rip]
        movsd   QWORD PTR [rbp-8], xmm0
        movsd   xmm0, QWORD PTR [rbp-8]
        mov     edi, OFFSET FLAT:.LC1
        mov     eax, 1
        call    printf
        mov     eax, 0
        leave
        ret
.LC0:
        .long   3100958126
        .long   1075678820

代碼二:

.LC0:
        .string "%lf\n"
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 32
        mov     DWORD PTR [rbp-20], edi
        mov     QWORD PTR [rbp-32], rsi
        mov     DWORD PTR [rbp-4], 2
        cvtsi2sd        xmm0, DWORD PTR [rbp-4]
        call    exp
        movq    rax, xmm0
        mov     QWORD PTR [rbp-16], rax
        movsd   xmm0, QWORD PTR [rbp-16]
        mov     edi, OFFSET FLAT:.LC0
        mov     eax, 1
        call    printf
        mov     eax, 0
        leave
        ret

彙編的具體細節我們無需盡知,但是我們可以很明顯地看到,第二段代碼調用了exp函數(call exp指令),而第一段代碼沒有看到調用exp的身影。
實際上,通過彙編代碼可以看到,當傳入參數爲常量時,就已經計算好了值(emm0寄存器爲浮點運算相關寄存器),最後根本不需要調用exp函數。而對於變量型的參數,其值在運行時確定,因此需要調用。我們還可以通過ldd命令來看它們鏈接的庫有什麼不同。
對於代碼一:

ldd expTest
    linux-vdso.so.1 =>  (0x00007ffec079d000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd327744000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fd327b0e000)

對於代碼二:

ldd expTest
    linux-vdso.so.1 =>  (0x00007ffefdfc9000)
    libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f9afcccb000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9afc901000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f9afcfd4000)

可以看到,第二段代碼編譯出來的可執行文件,多依賴了libm.so.6,也就是exp函數所在的庫。


2.什麼時候需要鏈接?


事實上,C編譯器總是主動傳送libc.a或libc.so給鏈接器,也就是說,對於使用包含在libc.a或libc.so庫中的函數,是不需要在編譯時手動鏈接的。而調用函數是否需要鏈接,可以使用命令“man 3 函數名“查看,如果需要鏈接庫,最後都有說明。

3.爲什麼鏈接的時候放在前面就不行呢?

這個就涉及到鏈接器的工作原理了,在此只簡單說明一下:鏈接過程中,需要進行符號解析,並且是按照順序解析;如果庫鏈接在前,就可能出現庫中的符號不會被需要,鏈接器不會把它加到未解析的符號集合中,那麼後面引用這個符號的目標文件就不能解析該引用,導致最後鏈接失敗。因此鏈接庫的一般準則是將它們放在命令行的結尾。

總結

通過前面的實例和分析,我們總結出以下幾點:

調用包含於libc庫中的函數不需要鏈接。
對於傳參爲常量的數學函數調用,生成可執行文件過程中可能將其優化,而無需調用該函數。
庫鏈接一般放在命令行結尾。
通過man命令查看在調用某個函數時是否需要鏈接。

發佈了9 篇原創文章 · 獲贊 4 · 訪問量 1367
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章