再議易語言靜態編譯重定位數目過多

20180629 Liigo 補記:此問題已得到完美解決,詳見本文最後一節《皆大歡喜》。

問題再現

數日之前有朋友聯繫我,說他的軟件靜態編譯後無法正常啓動,已經困擾了三天,多方求助,最後沒辦法了纔打電話給我。

據說該軟件已經持續開發維護了10多年,用到很多易語言支持庫和模塊。根據無法啓動的現象,聯想到我2015年的一篇博客《靜態編譯的EXE重定位項不能多於65535個》,我懷疑是靜態編譯時重定位項過多引起的。

根據我提供的線索,作者很快找到了易語言論壇的這篇帖子,很快就解決了問題。

無謂的惶恐

關於“靜態編譯的EXE重定位項不能多於65535個”這個問題的來龍去脈,我以前的博客裏講過。
自那以後,很多朋友對65535這個數目的限定表示擔憂,認爲一旦軟件做大之後很可能會突破此極限導致編譯失敗。

其中以這篇帖子爲典型,措辭較爲激烈。但是這篇帖子有兩個致命的概念錯誤讓我不屑一顧:第一,他把EXE/PE中的重定位項跟OBJ/COFF中某Section內的重定位項弄混了(評論中曾有網友指出這一點,可惜沒幾個人看得懂),一個在Link之後一個在Link之前,差的不是一般的遠,居然還裝模做樣的弄一個檢測EXE/DLL重定位數的程序來糊弄人。第二,他把 /bigobj 拿出來說“微軟解決了這個問題”,然而 /bigobj 解決的是“OBJ內Section數目受限”,而非“Section內重定位數目受限”呀,很明顯不是同一個問題。

如果要問這篇帖子的價值,我認爲無非就是給那些不明真相的羣衆無故增加了莫名的惶恐心理。

其實沒必要惶恐。

前幾天看過一個新聞(當然也是舊聞),說40億年後銀河系可能和仙女座星系相撞,屆時地球的命運難測。因此而惶恐,就是杞人憂天。

重定位項過多的問題,值得擔憂,卻不值得惶恐。用易語言開發的軟件,大多是中小型軟件,做大的恐怕不多,真正導致重定位項爆棚的實例屈指可數就是明證。多年以後真的做大了,遇到問題了,也有具體可行的解決方案,不用擔心天塌下來,天塌不下來。

我三年前就說過了:

把函數/子程序分割,提煉出小的函數/子程序,通過這種方式減少重定位項的數目

事實證明這種方案是切實可行的。只是不太具體。後文將提供更加具體的方案。

聲明:我不反對更新易語言修正此問題。我早就從易語言公司離職多年,要改也不歸我管,我憑什麼反對呀。能不能改,要不要改,改的話採用什麼方案,都是現任開發者要考慮的問題。我只是從技術上做一點點分析,發表一點個人的看法。

重定位的來源

根據我(Liigo)的分析結果,整理出以下表格供大家參考。分析方法詳見下文。

1. 變量/常量/立即數

類目 重定位 備註
全局變量 程序集變量 每出現一次,增加一次重定位 g1 = 1f(g1)
子程序內的局部變量 不增加重定位
類模塊內的私有成員和局部變量 不增加重定位
自定義類型成員變量 不增加重定位
文本和字節集立即數 每出現一次,增加一次重定位 a = "liigo"b = {1,2,3}
文本和字節集 #常量 每出現一次,增加一次重定位 a = #換行符a = #長文本常量b = {}
圖片和聲音資源常量 每出現一次,增加一次重定位 b = #圖片1b = #聲音1
數值和邏輯類型的立即數和常量 不增加重定位 包括小數和雙精度小數以及各種整數

小結1:局部變量不增加重定位,數值邏輯常量不增加重定位,全局變量和文本字節集常量增加重定位。

注:表中所述“每出現一次”,是指在程序源代碼中使用某項目的次數。如 g1 = f(g1)g1 出現兩次, f 出現一次;又如:f("", "", "") 中文本立即數 "" 出現三次。

再如下面的循環代碼,雖然會循環執行 100 遍,但 f 在源代碼內僅出現一次。

.計次循環首 (100, )
    f ()
.計次循環尾 ()

2. 函數/子程序/方法

類目 重定位 備註
調用子程序 不增加重定位 包括跨程序集調用
調用DLL命令 不增加重定位 包括在外部模塊定義的DLL
調用類模塊方法 不增加重定位
調用外部模塊內子程序 不增加重定位
調用外部模塊內類方法 不增加重定位
調用支持庫內函數 每出現一次,增加一次重定位
調用支持庫內數據類型方法 每出現一次,增加一次重定位

小結2:調用支持庫定義的函數/方法增加重定位,調用易語言自己定義的子程序/方法/命令不增加重定位。

3. 組件屬性

使用組件屬性不增加重定位。

疊加效果

全局變量.方法("參數")

上面一行代碼至少增加3個重定位:

  • 使用全局變量一次
  • 調用支持庫方法一次
  • 使用文本立即數一次

“第一次出現”例外

前面表格中統計的對重定位的增減,都是針對“非第一次出現”而言。“第一次出現”屬於例外,可能會導致增加不止一次重定位。

以第一次調用某個模塊的子程序爲例,雖然該調用本身不增加重定位,但該子程序的函數體代碼需要被編譯進來,其內部使用的其他所有相關代碼也要編譯進來,自然要增加多項重定位,這也算是一種疊加效果。

第一次調用某個模塊的子程序/方法,可能會一次性引入數十數百甚至成千上萬的重定位。具體數目取決於該代碼塊以及相關代碼塊內所有疊加效果之和。

第一調用某個支持庫函數/方法,僅僅引入極少數重定位。因爲支持庫內相關的重定位存在於其自身obj內部,獨立於易語言編譯生成的obj。

總結

增加重定位的情況:

  • 使用全局變量、程序集變量
  • 使用文本和字節集類型的立即數和常量
  • 調用支持庫內的函數和方法

不增加重定位的情況:

  • 使用局部變量、類模塊私有成員、組件屬性
  • 使用非文本非字節集的立即數和常量
  • 調用子程序、調用類模塊方法、調用DLL命令
  • 調用支持庫函數和方法
  • 調用外部模塊的子程序和方法(但可能附帶增加大量重定位)

[注意] 隱含增加(大量)重定位的情況:調用外部模塊的子程序和方法。

編譯器需要優化的地方

下面的代碼,對全局變量 a 連續賦值相同的文本,按說應該等價於一行 a = "",只要兩次重定位就行(使用全局變量和文本立即數各一次),實際上生成了20次重定位。編譯器可考慮消除無效代碼,減少不必要的重定位。

a = ""
a = ""
a = ""
a = ""
a = ""
a = ""
a = ""
a = ""
a = ""
a = ""

再比如下面,返回() 後面的代碼必然是無效代碼,也應在被消除之列:

返回 ()
信息框 (“測試一下,這個東西能不能編譯。”, 0, )
信息框 (“測試一下,這個東西能不能編譯。”, 0, )
信息框 (“測試一下,這個東西能不能編譯。”, 0, )
信息框 (“測試一下,這個東西能不能編譯。”, 0, )

再比如,ecode段和econst段有許多互相交叉重定位的情況(A重定位至B,B重定位至A),是否可以優化掉?

分析方法

修改易語言安裝目錄 tools 子目錄內的 link.ini 文件,打開以下選項:

retain_intermediate_files=yes
;  retain_intermediate_files用於設置是否保留鏈接期間生成的中間文件(比如 obj,res,lib 等文件)。
;  可以設置爲 yes 或 no。默認值爲no,即不保留中間文件。

這樣在靜態編譯之後,我們能夠看到易語言編譯生成的 .obj 文件,文件名與EXE文件名一致。

利用工具 dumpbin.exe(來自Visual Studio安裝目錄) 查看 obj 文件結構詳情,將結果導出到文本文件:

dumpbin /all e.obj > obj.txt

打開文本文件 obj.txt 搜索 "number of relocations" 即可得知各Section內的重定位項數目,注意該數值以十六進制顯示。

SECTION HEADER #8
   .text name
       0 physical address
       0 virtual address
  769C5F size of raw data
   B6E7C file pointer to raw data (000B6E7C to 00820ADA)
    B166 file pointer to relocation table
       0 file pointer to line numbers
     1BD number of relocations       <<==== 重定位項看這裏
       0 number of line numbers
60300020 flags
         Code
         4 byte align
         Execute Read

RAW DATA #8
  00000000: 5E 6A 00 4B 75 FB FF E6 55 8B EC 81 EC 04 00 00  ^j.Ku???U.ì.ì...
  00000010: 00 89 65 FC 68 00 00 00 00 B8 00 00 00 00 E8 2A  ..eüh....?....è*
  ......

手工解決辦法

隨着軟件的不斷開發,功能越來越多,代碼越來越多,重定位也越來越多。大致來說,重定位數量是隨代碼量不斷增長的,是一個線性增長的過程。當重定位數量超越極限數值之後,軟件必然無法啓動,能很快被軟件開發者發現;而此時重定位的數量必然是剛剛越過極限,只要少量的減少重定位就能使其回到極限數量以內。這是我們能夠快速有效解決該問題的基本原理。

動手之前先統計哪個項目引入的重定位最多,槍打出頭鳥,效果更明顯。舉個例子,如果發現代碼中大量多次使用某個全局變量,或大量多次使用某個支持庫函數/方法,那它就是我們要找的那個出頭鳥。注意是代碼中的使用次數,即上文所述出現次數,而非運行時被執行的次數。

把重複代碼變成循環

原理:把“出現N次”變成“出現1次”。

信息框 (“測試一下,這個東西能不能編譯。”, 0, )
信息框 (“測試一下,這個東西能不能編譯。”, 0, )
信息框 (“測試一下,這個東西能不能編譯。”, 0, )
信息框 (“測試一下,這個東西能不能編譯。”, 0, )
信息框 (“測試一下,這個東西能不能編譯。”, 0, )
信息框 (“測試一下,這個東西能不能編譯。”, 0, )
此處省略代碼一萬行...

改成循環後執行效果是一樣的,但重定位數少了一萬倍:

.計次循環首 (10006, )
    信息框 (“測試一下,這個東西能不能編譯。”, 0, )
.計次循環尾 ()

把全局變量變成局部變量

原理:把全局變量“出現N次”,變成局部變量“出現N次” + 全局變量“出現1次”。

原理:使用全局變量增加重定位,而局部變量不增加。

信息框 (全局變量, 0, )
信息框 (全局變量, 0, )
信息框 (全局變量, 0, )
此處省略代碼一萬行...

引入一個新的局部變量,記錄全局變量的值,然後把所有全局變量變成局部變量,執行結果不變,重定位數從N減小到1:

局部變量 = 全局變量
信息框 (局部變量, 0, )
信息框 (局部變量, 0, )
信息框 (局部變量, 0, )
此處省略代碼一萬行...

把全局變量變成子程序

原理:把全局變量“出現N次”,變成在子程序內“出現2次”。

原理:使用全局變量增加重定位,而調用子程序不增加重定位。

全局變量 = 1
信息框 (全局變量, 0, )
全局變量 = 2
信息框 (全局變量, 0, )
全局變量 = 3
信息框 (全局變量, 0, )
此處省略代碼一萬行...

新增兩個子程序分別讀取和設置全局變量的值,然後用到該全局變量的情況,統統改成調用子程序,這樣修改之後,執行結果不變,但是重定位數由N減少到2:

置全局變量 (1)
信息框 (取全局變量 (), 0, )
置全局變量 (2)
信息框 (取全局變量 (), 0, )
置全局變量 (3)
信息框 (取全局變量 (), 0, )
此處省略代碼一萬行...

.子程序 置全局變量
.參數 參數
全局變量 = 參數

.子程序 取全局變量
返回 (全局變量)

缺點:會影響程序運行性能。

把調用支持庫函數方法變成調用子程序

原理:調用支持庫函數和方法增加重定位,調用內部子程序不增加重定位。

信息框 (“信息1”, 0, )
信息框 (“信息2”, 0, )
信息框 (信息變量, 0, )
信息框 (“信息x”, 0, )
信息框 (“信息y”, 0, )
信息框 (“信息信息zz”, 0, )
此處省略代碼一萬行...

引入一個新的封裝子程序,其內部的支持庫函數(或方法)調用只需“出現一次”,而封裝子程序雖然“出現N次”但不增加重定位。
如此修改後,重定位數目縮減N倍。

自定義信息框 (“信息1”, 0, )
自定義信息框 (“信息2”, 0, )
自定義信息框 (信息變量, 0, )
自定義信息框 (“信息x”, 0, )
自定義信息框 (“信息y”, 0, )
自定義信息框 (“信息信息zz”, 0, )
此處省略代碼一萬行...


.子程序 自定義信息框
.參數 信息文本, 文本型
.參數 按鈕, 整數型, 可空
.參數 窗口標題, 文本型, 可空

信息框 (信息文本, 按鈕, 窗口標題, )

缺點:會影響程序運行性能。

把立即數或常量變成局部變量或子程序

參考“把全局變量變成局部變量”“把全局變量變成子程序”。

放棄使用某個模塊

原理:模塊內部代碼可能引入大量重定位,而這些代碼不受我們控制。

原理:模塊比支持庫引入的重定位要多的多。

原理:把EXE內部分代碼轉移到DLL內,該DLL有另外65535個重定位可用。

在主程序EXE裏放棄使用某個模塊,把對應的使用該模塊的代碼移入新的DLL裏面,不夠的話還可以分出第二第三個DLL,每個DLL都有65535個重定位可用。

接受現實

對絕大多數易語言代碼而言,65535個重定位數目足夠使用。平時犯不着爲其不夠用而擔心。萬一以後真的不夠用了,還有的是辦法解決。本文就爲普通易語言開發者提供了具體切實可行的手工解決方案,操作上並無難度,適用於所有開發者。總之這個事兒根本就不叫事兒,該吃吃,該喝喝,別鑽牛角尖。

皆大歡喜 [增補]

本節內容爲 2018/06/29 增補。

其實微軟還真的是已經解決了這個問題。現在我蠻後悔在以前的那篇博文中盲目的錯誤的說了下面這些話:

所以這個問題根本就是無解。歸根揭底是C/C++編譯鏈接系統COFF格式OBJ文件結構設計不合理。

這是因爲我知識點有欠缺,沒能及時意識到其實已經存在現成的解決方案。直到昨天吳總告訴我IMAGE_SCN_LNK_NRELOC_OVFL,我才恍然大悟。

IMAGE_SCN_LNK_NRELOC_OVFL 0x01000000

The section contains extended relocations. The count of relocations for the section exceeds the 16 bits that is reserved for it in the section header. If the NumberOfRelocations field in the section header is 0xffff, the actual relocation count is stored in the VirtualAddress field of the first relocation. It is an error if IMAGE_SCN_LNK_NRELOC_OVFL is set and there are fewer than 0xffff relocations in the section.

https://docs.microsoft.com/zh-cn/windows/desktop/api/winnt/ns-winnt-_image_section_header

這應該是最好的結果。易語言編譯部分無需改動,鏈接部分只需很小的改動,用戶的易語言源代碼無需改動。升級之後支持幾百萬上千萬的重定位都是小意思。

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