※ 引述《knko ()》之銘言:
: 我在使用kubuntu(x86_64)編譯此套軟體時執行pmake all install時發現錯誤,原因是/u
: sr/bin/ld: /tmp/writemoney-42e226.o:/home/bbs/pttbbs/util/writemoney.c:5: multip
: le definition of `now'; util_var.o:/home/bbs/pttbbs/util/../mbbsd/var.c:374: fir
: st defined here,是於pttbbs.conf中有加入#define SHMALIGNEDSIZE (1048576*4)
: #define TIMET64,似乎是爲重複定義但是我不確定是軟體還是我設定的問題,我使用的版
: 本最後的編輯是https://github.com/ptt/pttbbs/commit/576513c502a9bf5fcfa08ae52ee94
: ed0c67be608
: ----
: Sent from BePTT on my Samsung SM-M127F
此問題與在 pttbbs.conf 中使用了哪些 `#define`s 無關。
「重複定義」
────────────────────────────────
‧ util/writemoney.c: Line 5–6:
time4_t now; // C: Tentative definition // C++: 變數定義
extern SHM_t *SHM; // 變數宣告 (不建立物件)
‧ include/var.h: Line 96–100:
extern time4_t now; // 變數宣告 (不建立物件)
extern int KEY_ESC_arg;
extern int watermode ;
extern int wmofo ;
extern SHM_t *SHM; // 變數宣告 (不建立物件)
‧ mbbsd/var.c: Line 373–375:
/* io.c */
time4_t now; // C: Tentative definition // C++: 變數定義
int KEY_ESC_arg; // C: Tentative definition // C++: 變數定義
Tentative Definition
────────────────────────────────
Tentative definition 是 C 特有的概念,
指既能作宣告也能作定義的函式外變數宣告或定義。
大部份的既沒有使用 `extern` 且沒有用 `=` 初始化的函式外變數宣告/定義屬於此類。
(函式沒有 tentative definition,而且函式宣告預設會被視爲已有 `extern`)
(注意:將 tentative definition 加上 `static` 後仍爲 tentative definition)
C++ 中無此概念,這樣寫的宣告/定義會被視爲定義,會建立被初始化爲 0 的物件;
而要進行函式外變數宣告時,則必須使用 `extern` 關鍵字
(可搭配匿名 `namespace` 以限制被宣告的變數的作用域爲目前檔案)。
由於 C++ 有 one definition rule,一個變數/函式在整個程式中僅能有 <= 1 個定義,
因此若編譯器發現單一個「檔案」(嚴謹地說,是 translation unit)
具有 > 1 個這樣的變數定義時,會發出編譯時期錯誤。
(上述程式碼中並沒有這樣的問題)
C 對 tentative definition 有特別的處理規則:
如果編譯器編譯這個檔案時,未找到任何此變數的只能作定義的定義,
才會爲此宣告/定義建立物件。
建置失敗的過程
────────────────────────────────
util/writemoney.c 與 mbbsd/var.c 間,沒有一方被另一方 `#include`d 的關係;
編譯器編譯其中一個時,看不到另一個檔案中的定義,因此並不會發出編譯時期錯誤。
您所遇到的建置錯誤是連結時期錯誤 (link-time error),
而不是編譯時期錯誤 (compile-time error)。
問題來了,
「重複定義」的連結
────────────────────────────────
‧ util/Makefile:
‧ Line 11–29:
UTIL_OBJS= util_var.o # 編譯後的 var.c(含有 `now` 的定義)
(空行)
MBBSD_OBJS= var # var.c
(空行)
# 下面這些程式, 會被 compile 並且和 $(UTIL_OBJS) 聯結 # 聯 → 連
CPROG_WITH_UTIL= \
(Line 17–20 略)
toplazyBM writemoney \ # writemoney.c(含有 `now` 的定義)
(Line 22–27 略)
munin useractive_munin \
(空行)
‧ Line 67–70:
.for fn in ${MBBSD_OBJS} # ${fn}.c == var.c
util_${fn}.o: ${BBSBASE} $(SRCROOT)/mbbsd/${fn}.c # 產生 util_var.o
${CC} ${CFLAGS} -D_BBS_UTIL_C_ -c -o $@ $(SRCROOT)/mbbsd/${fn}.c
.endfor
‧ Line 57–60:
.for fn in ${CPROG_WITH_UTIL} # ${fn}.c 會 == writemoney.c
${fn}: ${BBSBASE} ${fn}.c ${UTIL_OBJS} # ${UTIL_OBJS} == util_var.o
${CC} ${CFLAGS} ${LDFLAGS} -o ${fn} ${UTIL_OBJS} ${fn}.c $(LDLIBS)
.endfor
紅色的行中,${CC} (`gcc` 或 `clang`) 會去執行編譯器與連結器。
其中,連結器在連結 util_var.o 與 writemoney.c 時,
會發現 `now` 具有兩個 tentative definitions 的問題。
-fcommon & -fno-common
────────────────────────────────
`-fcommon` 選項(不可用於動態連結庫 (以 `--shared` 選項編譯的))
會使編譯器發現某變數具有 tentative definition(s) 但未找到只能作定義的定義時,
不直接建立對應物件,而到連結器連結時
才爲此變數的所有 tentative definition(s) 建立單一個被初始化爲 0 的對應物件。
`-fno-common` 選項(從 GCC 10 開始的預設值)
則會使編譯器發現某個檔案有這樣的狀況時,
就直接建立被初始化爲 0 的物件,使得連結器連結時發出連結時期錯誤。
ISO C 僅要求一個變數在整支程式中只能有 <= 1 個被初始化的定義,
但未規定 > 1 個檔案具有 tentative definition(s) 時要如何處理。
因此 ISO C 並不保證這樣的程式能夠被正確地建置或執行;
這樣的程式可能會隨實際的建置環境不同,而有不同的建置或執行結果。
也就是說,在 ISO C 標準中,這會造成未定義行爲 (undefined behavior)。
只是 GCC 額外提供了編譯選項,使得這種情況在標準之外變得有定義。
此外,由於 `-fcommon` 只對 tentative definition 有效,
如果 > 1 個檔案具有只能作定義的定義,或是以 C++ 模式編譯的話,
使用 `-fcommon` 選項編譯並無法避免連結時期錯誤。
※ 引述《holishing ( )》之銘言:
: 新版的 gcc 會嚴格限制 multiple definition
: 所以在 Ubuntu Focal 或 Debian Bullseye 會遇到編譯錯誤 (以前只會警告)
: 兩種解法:
: 第一種是在編譯參數加上 -fcommon (讓它允許重複定義)
: 第二種是把重複定義刪掉,例如參考以下修改:
: https://github.com/bbsdocker/imageptt/blob/87c0ec3c/multipledef.patch
: 應該就可以編譯過了
根本的解決方法
────────────────────────────────
1. 直接移除此 tentative definition。
‧ 因已由 `#include "bbs.h"` → `#include "var.h"` 引入宣告,
此 tentative definition 並非必要。
2. 若要保留此宣告,須加上 `extern`:
‧ util/writemoney.c: Line 5–6:
- time4_t now; // C: Tentative definition // C++: 定義
+ extern time4_t now; // 宣告 (不建立物件)
extern SHM_t *SHM; // 宣告 (不建立物件)
爲什麼使用了 tentative definition?
────────────────────────────────
在 ISO C89 以前,C 沒有 `extern` 可用,只能用此方法進行前向變數宣告;
而 ISO C89 引入 `extern` 後,爲了維持向舊相容,才引入了這項規則。
見 https://en.cppreference.com/w/c/language/extern
或許 writemoney.c 中的 `now` 的 tentative definition 的意圖是進行變數宣告。
也或許只是因爲當時的編譯器不會提出錯誤訊息,所以才沒有加上 `extern`。
‧ Google 搜尋在 1997 年推出(當時最流行的瀏覽器是 Netscape Navigator):
https://web.archive.org/web/19981111183552/http://google.stanford.edu/
‧ Wikipedia 在 2001 年推出(最流行 Microsoft Internet Explorer 5):
https://web.archive.org/web/20010727112808/http://www.wikipedia.org/
‧ cppreference.com 也在 2001 年推出:
https://web.archive.org/web/20010223150424/http://www.cppreference.com/
‧ writemoney.c 是在 2005 年撰寫的(最流行 Microsoft Internet Explorer 6):
https://github.com/ptt/pttbbs/blob/814f94b737/util/writemoney.c
當時使用瀏覽器瀏覽網頁,甚至使用搜尋引擎,或許還不是很流行的事情。
當時的開發者或許沒有如 cppreference.com 般唾手可得的語言標準參考資料能夠參考。
話雖如此,以上也只是對 16 年前的情況的猜測。也許眞相早已隨著時光而流逝。
--
※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 140.116.130.29 (臺灣)
※ 文章網址: https://www.ptt.cc/bbs/PttCurrent/M.1640012985.A.9F7.html
簡言之:
UTIL_OBJS= util_var.o # applepen
CPROG_WITH_UTIL= \ toplazyBM writemoney \ # pineapplepen
> ${CC} ${CFLAGS} ${LDFLAGS} -o ${fn} ${UTIL_OBJS} ${fn}.c $(LDLIBS)<
# PENPINEAPPLEAPPLEPEN
※ 編輯: IepID (140.116.130.29 臺灣), 12/20/2021 23:24:55
※ 編輯: IepID (140.116.130.29 臺灣), 12/20/2021 23:29:24
※ 編輯: IepID (140.116.130.29 臺灣), 12/20/2021 23:36:32
※ 編輯: IepID (140.116.130.29 臺灣), 12/20/2021 23:44:26
※ 編輯: IepID (140.116.130.29 臺灣), 12/20/2021 23:46:00