1 問題場景
大家都知道,我們在開發(fā)單片機(jī)類的嵌入式固件時(shí),一般使用的FLASH存儲空間都是比較有限的,小的可能幾十KB,大一點(diǎn)的可能也就幾百KB,可以說是寸金寸土的FLASH空間,可容不得我們半點(diǎn)垃圾代碼。 如果我們在寫代碼的過程中,隨便寫一些沒用的代碼,比如一些測試代碼,最后版本釋放的時(shí)候,這些測試代碼又沒有刪掉,還是參與了編譯,那么勢必最后這個(gè)函數(shù)的代碼實(shí)現(xiàn)就會保留在我們的固件包里面,這樣我們的固件包的bin文件大小勢必會增加,這顯然不是我們想要的。 另外,還有一種場景下,有些函數(shù)我們使用static修飾的局部函數(shù),只在初始化的時(shí)候通過初始化列表的形式調(diào)用一下,比如RT-Thread的初始化實(shí)現(xiàn),INIT_DEVICE_EXPORT(device_init_func)
,那么我們是不希望這個(gè)函數(shù)被優(yōu)化掉的,否則最后會出邏輯問題。 在使用GCC作為編譯器的環(huán)境下,有什么辦法可以實(shí)現(xiàn)呢?
2 需求分析
這里的需求兩點(diǎn):
- 沒有被調(diào)用的函數(shù)需要移除,不出現(xiàn)在最后的固件文件里面;
- 某些特殊的函數(shù)實(shí)現(xiàn),沒有被顯式調(diào)用,但是需要保留它,不能被優(yōu)化掉。
3 需求實(shí)現(xiàn)
3.1 示例代碼
實(shí)現(xiàn)的一個(gè)示例代碼如下所示,功能很簡單就定義了2個(gè)沒被調(diào)用的函數(shù),一個(gè)我希望優(yōu)化移除,一個(gè)我希望被優(yōu)化保留。
#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在鏈接所有目標(biāo)文件變成可執(zhí)行文件的時(shí)候,需要讀取的一個(gè)配置文件,該文件決定了最后的可執(zhí)行文件是如何分布的。 我這里使用的是Ubuntu X64平臺 GCC默認(rèn)的鏈接腳本,由此改造而來。 至于如何獲取GCC默認(rèn)的鏈接腳本,請參考這里的教程。 修改后的鏈接腳本如下:
/* 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_*) }
}
關(guān)鍵地方在于.textkeepused_code段的定義,其他都是默認(rèn)鏈接腳本就已有的內(nèi)容。
3.3 編譯腳本
Ubuntu x64下的編譯腳本,支持編譯啟用回收優(yōu)化和不啟用回收優(yōu)化的情況,參考如下:
#! /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 驗(yàn)證測試
3.4.1 驗(yàn)證不啟用編譯回收優(yōu)化的情況
使用./build.sh
編譯輸出各種文件,使用grep-rsn unused_func
驗(yàn)證:
**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
我們可以發(fā)現(xiàn),在最后生成的test可執(zhí)行文件中,都找到了unusedfunc1和testfunc2,也就是說在不啟用回收優(yōu)化的情況下,跟我們之前的預(yù)期是一樣的,這樣增加固件包的尺寸。
3.4.2 驗(yàn)證啟用編譯回收優(yōu)化的情況
使用./build.sh gc
編譯輸出各種文件,使用grep-rsn unused_func
驗(yàn)證:
**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**
從中,我們發(fā)現(xiàn)最后的可執(zhí)行文件testgc里面只有unusedfunc2,而unused_func1就沒回收了,這個(gè)就實(shí)現(xiàn)了我們前面定義的需求。
4 原理分析
4.1 實(shí)現(xiàn)原理
這里實(shí)現(xiàn)的原理主要有4個(gè)部分: 第1部分主要修改的是代碼編寫階段,在不希望被回收優(yōu)化的函數(shù)前面添加特殊的段名稱,比如__attribute__((section(".text.keep.used.code")))
, 第2部分主要修改的是編譯階段,通過在CFLAGS中添加-fdata-sections-ffunction-sections
來實(shí)現(xiàn), 第3部分主要修改的是鏈接階段,通過在LDFLAGS中添加-Wl,-gc-sections
來實(shí)現(xiàn), 第4部分主要修改的是鏈接腳本,通過在段名稱中,新增下面的段申明,主要是為了限制指定的段,不被回收。
.text_keep_used_code :
{
KEEP (*(.text.keep.used.code))
}
從原理上說,編譯時(shí)使用-fdata-sections-ffunction-sections
是為了讓data數(shù)據(jù)和每一個(gè)函數(shù)都生成特定的段,以函數(shù)xxx為例,那么它將會放在.text.xxx
段里面,然后在鏈接階段的時(shí)候使用-Wl,-gc-sections
回收那些不使用的段。 注意這里回收的最小單位是段,所以編譯階段那兩個(gè)選項(xiàng)是必不可少的。
4.2 原理驗(yàn)證分析
4.2.1 確認(rèn)編譯階段的函數(shù)所在的段
這個(gè)確認(rèn)我們可以map文件和.s匯編文件就可以確認(rèn),
/* 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
從上面的分析,可以知道函數(shù)的段分布是完全符合預(yù)期的。
4.2.2 確認(rèn)鏈接階段的函數(shù)所在的段的回收情況
為了驗(yàn)證這一點(diǎn),我們可以在LDFLAGS里面添加這一個(gè)選項(xiàng)-Wl,--print-gc-sections
,這樣我們就可以觀察到鏈接階段最后移除了那些沒有引用的段,從而確認(rèn)unusedfunc1和unusedfunc2是否被回收。 輸出的關(guān)鍵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 經(jīng)驗(yàn)總結(jié)
- 使用-gc-sections可以回收不使用的代碼段,從而減少代碼尺寸,降低固件占用FLASH的存儲空間;
- 在特定場景下,修改鏈接腳本可以實(shí)現(xiàn)某個(gè)函數(shù)不被鏈接優(yōu)化,達(dá)到特定的目的。
6 更多分享
本項(xiàng)目的所有測試代碼和編譯腳本,均可以在我的github倉庫01workstation中找到。
歡迎關(guān)注我的github倉庫01workstation,日常分享一些開發(fā)筆記和項(xiàng)目實(shí)戰(zhàn),歡迎指正問題。
同時(shí)也非常歡迎關(guān)注我的CSDN主頁和專欄:
【CSDN主頁:架構(gòu)師李肯】
【RT-Thread主頁:架構(gòu)師李肯】
【C/C++語言編程專欄】
【GCC專欄】
【信息安全專欄】
【RT-Thread開發(fā)筆記】
【freeRTOS開發(fā)筆記】
有問題的話,可以跟我討論,知無不答,謝謝大家。
審核編輯:湯梓紅
-
單片機(jī)
+關(guān)注
關(guān)注
6030文章
44500瀏覽量
632186 -
GCC
+關(guān)注
關(guān)注
0文章
105瀏覽量
24807 -
函數(shù)
+關(guān)注
關(guān)注
3文章
4286瀏覽量
62341
發(fā)布評論請先 登錄
相關(guān)推薦
評論