1 問題場景
大家都知道,我們在開發單片機類的嵌入式固件時,一般使用的FLASH存儲空間都是比較有限的,小的可能幾十KB,大一點的可能也就幾百KB,可以說是寸金寸土的FLASH空間,可容不得我們半點垃圾代碼。 如果我們在寫代碼的過程中,隨便寫一些沒用的代碼,比如一些測試代碼,最后版本釋放的時候,這些測試代碼又沒有刪掉,還是參與了編譯,那么勢必最后這個函數的代碼實現就會保留在我們的固件包里面,這樣我們的固件包的bin文件大小勢必會增加,這顯然不是我們想要的。 另外,還有一種場景下,有些函數我們使用static修飾的局部函數,只在初始化的時候通過初始化列表的形式調用一下,比如RT-Thread的初始化實現,INIT_DEVICE_EXPORT(device_init_func)
,那么我們是不希望這個函數被優化掉的,否則最后會出邏輯問題。 在使用GCC作為編譯器的環境下,有什么辦法可以實現呢?
2 需求分析
這里的需求兩點:
- 沒有被調用的函數需要移除,不出現在最后的固件文件里面;
- 某些特殊的函數實現,沒有被顯式調用,但是需要保留它,不能被優化掉。
3 需求實現
3.1 示例代碼
實現的一個示例代碼如下所示,功能很簡單就定義了2個沒被調用的函數,一個我希望優化移除,一個我希望被優化保留。
#include
#define CODE_SECTION(x) __attribute__((section(x)))
#define CODE_KEEP_USED CODE_SECTION(".text.keep.used.code")
void unused_func1(int a)
{
printf("a: %d\n", a);
}
CODE_KEEP_USED void unused_func2(int a)
{
printf("a: %d\n", a);
}
int main(int argc, const char *argv[])
{
printf("Hello world !\n");
return 0;
}
3.2 鏈接腳本
鏈接腳本是GCC在鏈接所有目標文件變成可執行文件的時候,需要讀取的一個配置文件,該文件決定了最后的可執行文件是如何分布的。 我這里使用的是Ubuntu X64平臺 GCC默認的鏈接腳本,由此改造而來。 至于如何獲取GCC默認的鏈接腳本,請參考這里的教程。 修改后的鏈接腳本如下:
/* Script for -z combreloc -z separate-code */
/* Copyright (C) 2014-2020 Free Software Foundation, Inc.
Copying and distribution of this script, with or without modification,
are permitted in any medium without royalty provided the copyright
notice and this notice are preserved. */
OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64",
"elf64-x86-64")
OUTPUT_ARCH(i386:x86-64)
ENTRY(_start)
SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu"); SEARCH_DIR("=/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/lib/x86_64-linux-gnu64"); SEARCH_DIR("=/usr/local/lib64"); SEARCH_DIR("=/lib64"); SEARCH_DIR("=/usr/lib64"); SEARCH_DIR("=/usr/local/lib"); SEARCH_DIR("=/lib"); SEARCH_DIR("=/usr/lib"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib64"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib");
SECTIONS
{
PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;
.interp : { *(.interp) }
.note.gnu.build-id : { *(.note.gnu.build-id) }
.hash : { *(.hash) }
.gnu.hash : { *(.gnu.hash) }
.dynsym : { *(.dynsym) }
.dynstr : { *(.dynstr) }
.gnu.version : { *(.gnu.version) }
.gnu.version_d : { *(.gnu.version_d) }
.gnu.version_r : { *(.gnu.version_r) }
.rela.dyn :
{
*(.rela.init)
*(.rela.text .rela.text.* .rela.gnu.linkonce.t.*)
*(.rela.fini)
*(.rela.rodata .rela.rodata.* .rela.gnu.linkonce.r.*)
*(.rela.data .rela.data.* .rela.gnu.linkonce.d.*)
*(.rela.tdata .rela.tdata.* .rela.gnu.linkonce.td.*)
*(.rela.tbss .rela.tbss.* .rela.gnu.linkonce.tb.*)
*(.rela.ctors)
*(.rela.dtors)
*(.rela.got)
*(.rela.bss .rela.bss.* .rela.gnu.linkonce.b.*)
*(.rela.ldata .rela.ldata.* .rela.gnu.linkonce.l.*)
*(.rela.lbss .rela.lbss.* .rela.gnu.linkonce.lb.*)
*(.rela.lrodata .rela.lrodata.* .rela.gnu.linkonce.lr.*)
*(.rela.ifunc)
}
.rela.plt :
{
*(.rela.plt)
PROVIDE_HIDDEN (__rela_iplt_start = .);
*(.rela.iplt)
PROVIDE_HIDDEN (__rela_iplt_end = .);
}
. = ALIGN(CONSTANT (MAXPAGESIZE));
.init :
{
KEEP (*(SORT_NONE(.init)))
}
.plt : { *(.plt) *(.iplt) }
.plt.got : { *(.plt.got) }
.plt.sec : { *(.plt.sec) }
.text :
{
*(.text.unlikely .text.*_unlikely .text.unlikely.*)
*(.text.exit .text.exit.*)
*(.text.startup .text.startup.*)
*(.text.hot .text.hot.*)
*(SORT(.text.sorted.*))
*(.text .stub .text.* .gnu.linkonce.t.*)
/* .gnu.warning sections are handled specially by elf.em. */
*(.gnu.warning)
}
.fini :
{
KEEP (*(SORT_NONE(.fini)))
}
PROVIDE (__etext = .);
PROVIDE (_etext = .);
PROVIDE (etext = .);
. = ALIGN(CONSTANT (MAXPAGESIZE));
/* Adjust the address for the rodata segment. We want to adjust up to
the same address within the page on the next page up. */
. = SEGMENT_START("rodata-segment", ALIGN(CONSTANT (MAXPAGESIZE)) + (. & (CONSTANT (MAXPAGESIZE) - 1)));
.rodata : { *(.rodata .rodata.* .gnu.linkonce.r.*) }
.rodata1 : { *(.rodata1) }
.eh_frame_hdr : { *(.eh_frame_hdr) *(.eh_frame_entry .eh_frame_entry.*) }
.eh_frame : ONLY_IF_RO { KEEP (*(.eh_frame)) *(.eh_frame.*) }
.gcc_except_table : ONLY_IF_RO { *(.gcc_except_table .gcc_except_table.*) }
.gnu_extab : ONLY_IF_RO { *(.gnu_extab*) }
/* These sections are generated by the Sun/Oracle C++ compiler. */
.exception_ranges : ONLY_IF_RO { *(.exception_ranges*) }
/* Adjust the address for the data segment. We want to adjust up to
the same address within the page on the next page up. */
. = DATA_SEGMENT_ALIGN (CONSTANT (MAXPAGESIZE), CONSTANT (COMMONPAGESIZE));
/* Exception handling */
.eh_frame : ONLY_IF_RW { KEEP (*(.eh_frame)) *(.eh_frame.*) }
.gnu_extab : ONLY_IF_RW { *(.gnu_extab) }
.gcc_except_table : ONLY_IF_RW { *(.gcc_except_table .gcc_except_table.*) }
.exception_ranges : ONLY_IF_RW { *(.exception_ranges*) }
/* Thread Local Storage sections */
.tdata :
{
PROVIDE_HIDDEN (__tdata_start = .);
*(.tdata .tdata.* .gnu.linkonce.td.*)
}
.tbss : { *(.tbss .tbss.* .gnu.linkonce.tb.*) *(.tcommon) }
.preinit_array :
{
PROVIDE_HIDDEN (__preinit_array_start = .);
KEEP (*(.preinit_array))
PROVIDE_HIDDEN (__preinit_array_end = .);
}
.init_array :
{
PROVIDE_HIDDEN (__init_array_start = .);
KEEP (*(SORT_BY_INIT_PRIORITY(.init_array.*) SORT_BY_INIT_PRIORITY(.ctors.*)))
KEEP (*(.init_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .ctors))
PROVIDE_HIDDEN (__init_array_end = .);
}
.fini_array :
{
PROVIDE_HIDDEN (__fini_array_start = .);
KEEP (*(SORT_BY_INIT_PRIORITY(.fini_array.*) SORT_BY_INIT_PRIORITY(.dtors.*)))
KEEP (*(.fini_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .dtors))
PROVIDE_HIDDEN (__fini_array_end = .);
}
/* Here use to keep some sections, which are not wanted to be removed. */
.text_keep_used_code :
{
KEEP (*(.text.keep.used.code))
}
.ctors :
{
/* gcc uses crtbegin.o to find the start of
the constructors, so we make sure it is
first. Because this is a wildcard, it
doesn't matter if the user does not
actually link against crtbegin.o; the
linker won't look for a file to match a
wildcard. The wildcard also means that it
doesn't matter which directory crtbegin.o
is in. */
KEEP (*crtbegin.o(.ctors))
KEEP (*crtbegin?.o(.ctors))
/* We don't want to include the .ctor section from
the crtend.o file until after the sorted ctors.
The .ctor section from the crtend file contains the
end of ctors marker and it must be last */
KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .ctors))
KEEP (*(SORT(.ctors.*)))
KEEP (*(.ctors))
}
.dtors :
{
KEEP (*crtbegin.o(.dtors))
KEEP (*crtbegin?.o(.dtors))
KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .dtors))
KEEP (*(SORT(.dtors.*)))
KEEP (*(.dtors))
}
.jcr : { KEEP (*(.jcr)) }
.data.rel.ro : { *(.data.rel.ro.local* .gnu.linkonce.d.rel.ro.local.*) *(.data.rel.ro .data.rel.ro.* .gnu.linkonce.d.rel.ro.*) }
.dynamic : { *(.dynamic) }
.got : { *(.got) *(.igot) }
. = DATA_SEGMENT_RELRO_END (SIZEOF (.got.plt) >= 24 ? 24 : 0, .);
.got.plt : { *(.got.plt) *(.igot.plt) }
.data :
{
*(.data .data.* .gnu.linkonce.d.*)
SORT(CONSTRUCTORS)
}
.data1 : { *(.data1) }
_edata = .; PROVIDE (edata = .);
. = .;
__bss_start = .;
.bss :
{
*(.dynbss)
*(.bss .bss.* .gnu.linkonce.b.*)
*(COMMON)
/* Align here to ensure that the .bss section occupies space up to
_end. Align after .bss to ensure correct alignment even if the
.bss section disappears because there are no input sections.
FIXME: Why do we need it? When there is no .bss section, we do not
pad the .data section. */
. = ALIGN(. != 0 ? 64 / 8 : 1);
}
.lbss :
{
*(.dynlbss)
*(.lbss .lbss.* .gnu.linkonce.lb.*)
*(LARGE_COMMON)
}
. = ALIGN(64 / 8);
. = SEGMENT_START("ldata-segment", .);
.lrodata ALIGN(CONSTANT (MAXPAGESIZE)) + (. & (CONSTANT (MAXPAGESIZE) - 1)) :
{
*(.lrodata .lrodata.* .gnu.linkonce.lr.*)
}
.ldata ALIGN(CONSTANT (MAXPAGESIZE)) + (. & (CONSTANT (MAXPAGESIZE) - 1)) :
{
*(.ldata .ldata.* .gnu.linkonce.l.*)
. = ALIGN(. != 0 ? 64 / 8 : 1);
}
. = ALIGN(64 / 8);
_end = .; PROVIDE (end = .);
. = DATA_SEGMENT_END (.);
/* Stabs debugging sections. */
.stab 0 : { *(.stab) }
.stabstr 0 : { *(.stabstr) }
.stab.excl 0 : { *(.stab.excl) }
.stab.exclstr 0 : { *(.stab.exclstr) }
.stab.index 0 : { *(.stab.index) }
.stab.indexstr 0 : { *(.stab.indexstr) }
.comment 0 : { *(.comment) }
.gnu.build.attributes : { *(.gnu.build.attributes .gnu.build.attributes.*) }
/* DWARF debug sections.
Symbols in the DWARF debugging sections are relative to the beginning
of the section so we begin them at 0. */
/* DWARF 1 */
.debug 0 : { *(.debug) }
.line 0 : { *(.line) }
/* GNU DWARF 1 extensions */
.debug_srcinfo 0 : { *(.debug_srcinfo) }
.debug_sfnames 0 : { *(.debug_sfnames) }
/* DWARF 1.1 and DWARF 2 */
.debug_aranges 0 : { *(.debug_aranges) }
.debug_pubnames 0 : { *(.debug_pubnames) }
/* DWARF 2 */
.debug_info 0 : { *(.debug_info .gnu.linkonce.wi.*) }
.debug_abbrev 0 : { *(.debug_abbrev) }
.debug_line 0 : { *(.debug_line .debug_line.* .debug_line_end) }
.debug_frame 0 : { *(.debug_frame) }
.debug_str 0 : { *(.debug_str) }
.debug_loc 0 : { *(.debug_loc) }
.debug_macinfo 0 : { *(.debug_macinfo) }
/* SGI/MIPS DWARF 2 extensions */
.debug_weaknames 0 : { *(.debug_weaknames) }
.debug_funcnames 0 : { *(.debug_funcnames) }
.debug_typenames 0 : { *(.debug_typenames) }
.debug_varnames 0 : { *(.debug_varnames) }
/* DWARF 3 */
.debug_pubtypes 0 : { *(.debug_pubtypes) }
.debug_ranges 0 : { *(.debug_ranges) }
/* DWARF Extension. */
.debug_macro 0 : { *(.debug_macro) }
.debug_addr 0 : { *(.debug_addr) }
.gnu.attributes 0 : { KEEP (*(.gnu.attributes)) }
/DISCARD/ : { *(.note.GNU-stack) *(.gnu_debuglink) *(.gnu.lto_*) }
}
關鍵地方在于.textkeepused_code段的定義,其他都是默認鏈接腳本就已有的內容。
3.3 編譯腳本
Ubuntu x64下的編譯腳本,支持編譯啟用回收優化和不啟用回收優化的情況,參考如下:
#! /bin/bash -e
CFLAGS="-save-temps=obj -Wall"
LDFLAGS="-Wl,-Map=test.map"
CFLAGS_GC="-fdata-sections -ffunction-sections"
LDFLAGS_GC="-Wl,--gc-sections"
LDFLAGS_MAP_GC="-Wl,-Map=test_gc.map"
PRINT_GC="-Wl,--print-gc-sections"
GCC_LDS=default.lds
if [ "$1" = "clean" ]; then
rm -rf test* *.i *.s *.o *.map
echo "Clean build done !"
exit 0
elif [ "$1" = "gc" ]; then
echo "gcc compile with gc ..."
single_c_file=`ls *.c | cut -d . -f 1`
cmd1="gcc -o $single_c_file.o -c *.c $CFLAGS $CFLAGS_GC"
cmd2="gcc -o test_gc $single_c_file.o $LDFLAGS_GC $LDFLAGS_MAP_GC -T $GCC_LDS $PRINT_GC"
echo "$cmd1 && $cmd2" && $cmd1 && $cmd2
else
echo "gcc compile without gc ... (default)"
cmd="gcc *.c $CFLAGS $LDFLAGS -o test"
echo $cmd && $cmd
fi
exit 0
3.4 驗證測試
3.4.1 驗證不啟用編譯回收優化的情況
使用./build.sh
編譯輸出各種文件,使用grep-rsn unused_func
驗證:
**grep -rsnw unused_func1**
main.c:9:void unused_func1(int a)
**Binary file test matches**
test.i:735:void unused_func1(int a)
Binary file test.o matches
test.s:7: .globl unused_func1
test.s:8: .type unused_func1, @function
test.s:9:unused_func1:
test.s:31: .size unused_func1, .-unused_func1
test.map:193: 0x0000000000001169 unused_func1
**grep -rsnw unused_func2**
main.c:14:CODE_KEEP_USED void unused_func2(int a)
**Binary file test matches**
test.i:740:__attribute__((section(".text.keep.used.code"))) void unused_func2(int a)
Binary file test.o matches
test.s:33: .globl unused_func2
test.s:34: .type unused_func2, @function
test.s:35:unused_func2:
test.s:57: .size unused_func2, .-unused_func2
test.map:197: 0x00000000000011b7 unused_func2
我們可以發現,在最后生成的test可執行文件中,都找到了unusedfunc1和testfunc2,也就是說在不啟用回收優化的情況下,跟我們之前的預期是一樣的,這樣增加固件包的尺寸。
3.4.2 驗證啟用編譯回收優化的情況
使用./build.sh gc
編譯輸出各種文件,使用grep-rsn unused_func
驗證:
**grep -rsnw unused_func1**
Binary file main.o matches
main.c:9:void unused_func1(int a)
test_gc.map:35: .text.unused_func1
main.i:735:void unused_func1(int a)
main.s:6: .section .text.unused_func1,"ax",@progbits
main.s:7: .globl unused_func1
main.s:8: .type unused_func1, @function
main.s:9:unused_func1:
main.s:31: .size unused_func1, .-unused_func1
**grep -rsnw unused_func2**
Binary file main.o matches
main.c:14:CODE_KEEP_USED void unused_func2(int a)
test_gc.map:215: 0x0000000000401169 unused_func2
main.i:740:__attribute__((section(".text.keep.used.code"))) void unused_func2(int a)
main.s:33: .globl unused_func2
main.s:34: .type unused_func2, @function
main.s:35:unused_func2:
main.s:57: .size unused_func2, .-unused_func2
**Binary file test_gc matches**
從中,我們發現最后的可執行文件testgc里面只有unusedfunc2,而unused_func1就沒回收了,這個就實現了我們前面定義的需求。
4 原理分析
4.1 實現原理
這里實現的原理主要有4個部分: 第1部分主要修改的是代碼編寫階段,在不希望被回收優化的函數前面添加特殊的段名稱,比如__attribute__((section(".text.keep.used.code")))
, 第2部分主要修改的是編譯階段,通過在CFLAGS中添加-fdata-sections-ffunction-sections
來實現, 第3部分主要修改的是鏈接階段,通過在LDFLAGS中添加-Wl,-gc-sections
來實現, 第4部分主要修改的是鏈接腳本,通過在段名稱中,新增下面的段申明,主要是為了限制指定的段,不被回收。
.text_keep_used_code :
{
KEEP (*(.text.keep.used.code))
}
從原理上說,編譯時使用-fdata-sections-ffunction-sections
是為了讓data數據和每一個函數都生成特定的段,以函數xxx為例,那么它將會放在.text.xxx
段里面,然后在鏈接階段的時候使用-Wl,-gc-sections
回收那些不使用的段。 注意這里回收的最小單位是段,所以編譯階段那兩個選項是必不可少的。
4.2 原理驗證分析
4.2.1 確認編譯階段的函數所在的段
這個確認我們可以map文件和.s匯編文件就可以確認,
/* map文件中 unused_fun1的描述 */
35 .text.unused_func1
36 0x0000000000000000 0x28 main.o
/* 文件中 unused_fun2的描述 */
213 .text.keep.used.code
214 0x0000000000401169 0x28 main.o
215 0x0000000000401169 unused_func2
/* 匯編文件中 unused_fun1的描述 */
6 .section .text.unused_func1,"ax",@progbits
7 .globl unused_func1
8 .type unused_func1, @function
9 unused_func1:
10 .LFB0:
11 .cfi_startproc
12 endbr64
13 pushq %rbp
14 .cfi_def_cfa_offset 16
15 .cfi_offset 6, -16
/* 匯編文件中 unused_fun2的描述 */
32 .section .text.keep.used.code,"ax",@progbits
33 .globl unused_func2
34 .type unused_func2, @function
35 unused_func2:
36 .LFB1:
37 .cfi_startproc
38 endbr64
39 pushq %rbp
40 .cfi_def_cfa_offset 16
41 .cfi_offset 6, -16
42 movq %rsp, %rbp
43 .cfi_def_cfa_register 6
從上面的分析,可以知道函數的段分布是完全符合預期的。
4.2.2 確認鏈接階段的函數所在的段的回收情況
為了驗證這一點,我們可以在LDFLAGS里面添加這一個選項-Wl,--print-gc-sections
,這樣我們就可以觀察到鏈接階段最后移除了那些沒有引用的段,從而確認unusedfunc1和unusedfunc2是否被回收。 輸出的關鍵log如下:
./build.sh gc
gcc compile with gc ...
gcc -o main.o -c *.c -save-temps=obj -Wall -fdata-sections -ffunction-sections && gcc -o test_gc main.o -Wl,--gc-sections -Wl,-Map=test_gc.map -T default.lds -Wl,--print-gc-sections
/usr/bin/ld: removing unused section '.rodata.cst4' in file '/usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/Scrt1.o'
/usr/bin/ld: removing unused section '.data' in file '/usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/Scrt1.o'
/usr/bin/ld: removing unused section '.text.unused_func1' in file 'main.o'
log很明顯就告訴我們,unused section '.text.unused_func1'被回收移除了,而.text.keep.used.code是沒有被回收的,這切好證明了我們的猜想。
5 經驗總結
- 使用-gc-sections可以回收不使用的代碼段,從而減少代碼尺寸,降低固件占用FLASH的存儲空間;
- 在特定場景下,修改鏈接腳本可以實現某個函數不被鏈接優化,達到特定的目的。
6 更多分享
本項目的所有測試代碼和編譯腳本,均可以在我的github倉庫01workstation中找到。
歡迎關注我的github倉庫01workstation,日常分享一些開發筆記和項目實戰,歡迎指正問題。
同時也非常歡迎關注我的CSDN主頁和專欄:
【CSDN主頁:架構師李肯】
【RT-Thread主頁:架構師李肯】
【C/C++語言編程專欄】
【GCC專欄】
【信息安全專欄】
【RT-Thread開發筆記】
【freeRTOS開發筆記】
有問題的話,可以跟我討論,知無不答,謝謝大家。
審核編輯:湯梓紅
-
單片機
+關注
關注
6040文章
44592瀏覽量
636891 -
GCC
+關注
關注
0文章
107瀏覽量
24857 -
函數
+關注
關注
3文章
4341瀏覽量
62806
發布評論請先 登錄
相關推薦
評論