深入理解ELF格式
2020-06-24 00:06 作者:無(wú)情劍客Burning | 我要投稿
每個(gè)操作系統(tǒng)都會(huì)有自己的可執(zhí)行文件的格式,比如以前的Unix?是用a.out格式的,現(xiàn)代的Unix?類(lèi)系統(tǒng)使用elf格式, WindowsNT?是使用基于COFF格式的可執(zhí)行文件。那么最簡(jiǎn)單的格式應(yīng)該是DOS的可執(zhí)行格式,嚴(yán)格來(lái)說(shuō)DOS的可執(zhí)行文件沒(méi)有什么格式可言,就是把二進(jìn)制代碼安順序放在文件里,運(yùn)行時(shí)DOS操作系統(tǒng)就把所有控制計(jì)算機(jī)的權(quán)力都給了這個(gè)程序。這種方式的不足之處是顯而易見(jiàn)的,所以現(xiàn)代的操作系統(tǒng)都有一種更好的方式來(lái)定義可執(zhí)行文件的格式。一種常見(jiàn)的方法就是為可執(zhí)行文件分段,一般來(lái)說(shuō)把程序指令的內(nèi)容放在.text段中,把程序中的數(shù)據(jù)內(nèi)容放在. data段中,把程序中未初始化的數(shù)據(jù)放在.bss段中。這種做法的好處有很多,可以讓操作系統(tǒng)內(nèi)核來(lái)檢查程序防止有嚴(yán)重錯(cuò)誤的程序破壞整個(gè)運(yùn)行環(huán)境。比如:某個(gè)程序想要修改.text段中的內(nèi)容,那么操作系統(tǒng)就會(huì)認(rèn)為這段程序有誤而立即終止它的運(yùn)行,因?yàn)橄到y(tǒng)會(huì)把.text段的內(nèi)存標(biāo)記為只讀。在. bss段中的數(shù)據(jù)還沒(méi)有初始化,就沒(méi)有必要在可執(zhí)行文件中浪費(fèi)儲(chǔ)存空間。在.bss中只是表明某個(gè)變量要使用多少的內(nèi)存空間,等到程序加載的時(shí)候在由內(nèi)核把這段未初始化的內(nèi)存空間初始化為0。這些就是分段儲(chǔ)存可執(zhí)行文件的內(nèi)容的好處。
下面談一下Unix系統(tǒng)里的兩種重要的格式:a.out和elf(Executable and Linking Format)。這兩種格式中都有符號(hào)表(symbol table),其中包括所有的符號(hào)(程序的入口點(diǎn)還有變量的地址等等)。在elf格式中符號(hào)表的內(nèi)容會(huì)比a.out格式的豐富的多。但是這些符號(hào)表可以用 strip工具去除,這樣的話這個(gè)文件就無(wú)法讓debug程序跟蹤了,但是會(huì)生成比較小的可執(zhí)行文件。a.out文件中的符號(hào)表可以被完全去除,但是 elf中的在加載運(yùn)行是起著重要的作用,所以用strip永遠(yuǎn)不可能完全去除elf格式文件中的符號(hào)表。但是用strip命令不是完全安全的,比如對(duì)未連接的目標(biāo)文件來(lái)說(shuō)如果用strip去掉符號(hào)表的話,會(huì)導(dǎo)致連接器無(wú)法連接。例如:
代碼:
$:gcc -c hello.c
$:ls hello.c hello.o
用gcc把hello.c編譯成目標(biāo)文件hello.o
代碼:
$:strip hello.o
用strip去掉hello.o中的符號(hào)信息。
代碼:
$:gcc hello.o /usr/lib/gcc/i686-pc-linux-gnu/3.4.5/../../../crt1.o: In function `_start': init.c: (.text+0x18) : undefined reference to `main' collect2: ld returned 1 exit status
再用gcc連接時(shí),連接器ld報(bào)錯(cuò)。說(shuō)明在目標(biāo)文件中的符號(hào)起著很重要的作用,如果要發(fā)布二進(jìn)制的程序的話,在debug后為了減小可執(zhí)行文件的大小,可以用strip來(lái)除去符號(hào)信息但是在程序的調(diào)試階段還是不要用strip為好。
在接下去討論以前,我們還要來(lái)講講relocations的概念:首先有個(gè)簡(jiǎn)單的程序hello.c
代碼:
$:cat hello.c
main( )
{
? printf("Hello World\n");
}
當(dāng)我們把hello.c編譯為目標(biāo)文件時(shí),我們并沒(méi)有在源文件中定義printf這個(gè)函數(shù),所以匯編器也不知道printf這個(gè)函數(shù)的具體的地址,所以在目標(biāo)文件中就會(huì)留下printf這個(gè)符號(hào)。以下的工作就交給連接器了,連接器會(huì)找到這個(gè)函數(shù)的入口地址然后傳遞給這個(gè)文件最終形成可執(zhí)行文件。這個(gè)過(guò)程就叫做relocations。a.out格式的可執(zhí)行文件是沒(méi)有這種relocation的功能的,內(nèi)核不會(huì)執(zhí)行其中還有未知函數(shù)的入口地址的可執(zhí)行文件的。在目標(biāo)文件中當(dāng)然可以relocation,只不過(guò)連接器需要把未知函數(shù)的入口地址完全找到,生成可執(zhí)行文件才行。這樣就有一個(gè)很尷尬的問(wèn)題,在 a.out格式中極其難以實(shí)現(xiàn)動(dòng)態(tài)連接技術(shù)。要知道為什么現(xiàn)在的Unix幾乎都是用的elf格式的可執(zhí)行文件就要了解a.out格式的短處。
a.out的符號(hào)是極其有限的,在/usr/include/linux/asm/a.out.h中定義了一個(gè)結(jié)構(gòu)exec就是:
代碼:
struct exec { unsigned long a_info; /*Use macros N_MAGIC, etc for access */ unsigned a_text; /* length of text, in bytes */ unsigned a_data; /* length of data, in bytes */ unsigned a_bss; /* length of uninitialized data area for file, in bytes*/ unsigned a_syms; /* length of symbol table data in file, in bytes */ unsigned a_entry; /* start address */ unsigned a_trsize; /*length of relocation info for text, in bytes */ unsigned a_drsize; /*length of relocation info for data, in bytes */ };
在這個(gè)結(jié)構(gòu)中更本沒(méi)有指示每個(gè)段在文件中的開(kāi)始位置,內(nèi)核加載器具有一些非正式的方法來(lái)加載可執(zhí)行文件的。明顯的,a.out 是不支持動(dòng)態(tài)連接的。(在內(nèi)部不支持動(dòng)態(tài)連接,用某些技術(shù)也是可以實(shí)現(xiàn)a.out的動(dòng)態(tài)連接)
要了解elf可執(zhí)行文件的運(yùn)行方式,我們有必要討論一下動(dòng)態(tài)連接技術(shù)。很多人對(duì)動(dòng)態(tài)連接技術(shù)十分熟悉,但是很少有人真正了解動(dòng)態(tài)連接的內(nèi)部工作方式?;叵霙](méi)有動(dòng)態(tài)連接的日子,程序員寫(xiě)程序時(shí)不用什么都從頭開(kāi)始,他們可以調(diào)用定義的很好的函數(shù),然后再用連接器與函數(shù)庫(kù)連接。這樣的話使得程序員更加有效率,但是一個(gè)十分重要的問(wèn)題出現(xiàn)了:這樣產(chǎn)生的可執(zhí)行文件就會(huì)很大。因?yàn)檫B接器把程序需要用的所有函數(shù)的代碼都復(fù)制到了可執(zhí)行文件中去了。這種連接方式就是所謂的靜態(tài)連接,與之相對(duì)的就是動(dòng)態(tài)連接。連接器在可執(zhí)行文件中標(biāo)記出程序調(diào)用外部函數(shù)的位置,并不把代碼復(fù)制進(jìn)去,只是標(biāo)出函數(shù)在動(dòng)態(tài)連接庫(kù)中的位置。用這樣的方式生成的特殊可執(zhí)行文件就是動(dòng)態(tài)連接的。在運(yùn)行這種動(dòng)態(tài)程序時(shí),系統(tǒng)在運(yùn)行時(shí)把該程序調(diào)用的外部函數(shù)地址映射到程序地址,這就是所謂的動(dòng)態(tài)連接,系統(tǒng)就有一個(gè)程序叫做動(dòng)態(tài)連接器,在動(dòng)態(tài)連接的程序執(zhí)行前都要先把地址映射好。很顯然的,必須有一種機(jī)制保證動(dòng)態(tài)連接的程序中的函數(shù)地址正確地指向了動(dòng)態(tài)連接庫(kù)的某個(gè)函數(shù)地址。這就需要討論一下elf可執(zhí)行文件格式處理動(dòng)態(tài)連接的機(jī)制了。
elf的動(dòng)態(tài)連接庫(kù)是內(nèi)存位置無(wú)關(guān)的,就是說(shuō)你可以把這個(gè)庫(kù)加載到內(nèi)存的任何位置都沒(méi)有影響。這就叫做position independent。而a.out的動(dòng)態(tài)連接庫(kù)是內(nèi)存位置有關(guān)的,它一定要被加載到規(guī)定的內(nèi)存地址才能工作。在編譯內(nèi)存位置無(wú)關(guān)的動(dòng)態(tài)連接庫(kù)時(shí),要給編譯器加上 -fpic選項(xiàng),讓編譯器產(chǎn)生的目標(biāo)文件是內(nèi)存位置無(wú)關(guān)的還會(huì)盡量減少對(duì)變量引用時(shí)使用絕對(duì)地址。把庫(kù)編譯成內(nèi)存位置無(wú)關(guān)會(huì)帶來(lái)一些花費(fèi),編譯器會(huì)保留一個(gè)寄存器來(lái)指向全局偏移量表(global offset table (or GOT for short)),這就會(huì)導(dǎo)致編譯器在優(yōu)化代碼時(shí)少了一個(gè)寄存器可以使用,但是在最壞的情況下這種性能的減少只有3%,在其他情況下是大大小于3%的。
Elf的另一個(gè)特點(diǎn)是它的動(dòng)態(tài)連接庫(kù)是在運(yùn)行時(shí)處理符號(hào)的,這是通過(guò)用符號(hào)表和再布置(relocation)表來(lái)實(shí)現(xiàn)的。在載入文件時(shí)并不能立即執(zhí)行,要在處理完符號(hào)表把所有的地址都relocation完后才可以執(zhí)行。這個(gè)聽(tīng)起來(lái)有點(diǎn)復(fù)雜而且可能導(dǎo)致文件運(yùn)行慢,不過(guò)對(duì)elf做了很大的優(yōu)化后,這種減慢已經(jīng)是微不足道的了。理論上說(shuō)不是用-fpic選項(xiàng)編譯出來(lái)的目標(biāo)文件也可以用作動(dòng)態(tài)連接庫(kù),但是在運(yùn)行時(shí)會(huì)需要做數(shù)目極大的 relocation,這是對(duì)運(yùn)行速度有極大影響的。這樣的程序性能是很差的,幾乎沒(méi)有可用性。
當(dāng)從動(dòng)態(tài)連接庫(kù)中讀一個(gè)全局變量時(shí)與從非-fpic編譯的目標(biāo)文件讀是不同的。讀動(dòng)態(tài)連接的庫(kù)中的變量是通過(guò)GOT來(lái)尋找到目標(biāo)變量的,GOT已經(jīng)由某一個(gè)寄存器指向了。GOT本生就是一個(gè)指針列表,找到GOT中的某一個(gè)指針就可以讀到所要的全局變量了,有了GOT我們要讀出一個(gè)變量只要做一次 relocation。
下面我們來(lái)看看elf文件中到底有些什么信息:
代碼:
$:cat hello.c
main()
{
? printf("Hello World\n");
}
?
$:gcc-elf -c hello.c
還是這個(gè)簡(jiǎn)單的程序,用gcc把它編譯成目標(biāo)文件hello.o。然后用readelf工具來(lái)探測(cè)一下elf文件的內(nèi)容。(readelf是在 binutils軟件包里的一個(gè)工具,大多數(shù)Linux發(fā)行版都包含它)
代碼:
$:readelf -h hello.o
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32 Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V ABI Version: 0
Type: REL (Relocatable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 256 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 40 (bytes)
Number of section headers: 11
Section header string table index: 8
-h選項(xiàng)是列出elf文件的頭信息。Magic:字段是一個(gè)標(biāo)識(shí)符,只要Magic字段是7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00的文件都是elf文件。Class:字段是表示elf的版本,這是一個(gè)32位的elf。Machine:字段是指出目標(biāo)文件的平臺(tái)信息,這里是 I386兼容平臺(tái)。其他的字段可以從其字面上看出它的意義,這里就不一一解釋了。
下面用-S選項(xiàng)列出段的頭信息:
代碼:
$:readelf -S hello.o
There are 11 section headers, starting at offset 0x100:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00000000 000034 00002a 00 AX 0 0 4
[ 2] .rel.text REL 00000000 000370 000010 08 9 1 4
[ 3] .data PROGBITS 00000000 000060 000000 00 WA 0 0 4
[ 4] .bss NOBITS 00000000 000060 000000 00 WA 0 0 4
[ 5] .rodata PROGBITS 00000000 000060 00000e 00 A 0 0 1
[ 6] .note.GNU-stack PROGBITS 00000000 00006e 000000 00 0 0 1 [ 7] .comment PROGBITS 00000000 00006e 00003e 00 0 0 1
[ 8] .shstrtab STRTAB 00000000 0000ac 000051 00 0 0 1
[ 9] .symtab SYMTAB 00000000 0002b8 0000a0 10 10 8 4
[10] .strtab STRTAB 00000000 000358 000015 00 0 0 1
Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)
Name字段顯示的是各個(gè)段的名字,Type顯示段的屬性,Addr是每個(gè)段載入虛擬內(nèi)存的位置,Off是每個(gè)段在目標(biāo)文件中的偏移位置,Size是每個(gè)段的大小,后面的一些字段是表示段的可寫(xiě),可讀,或者可執(zhí)行。
用-r可以列出elf文件中的relocation:
代碼:
$:readelf -r hello.o
Relocation section '.rel.text' at offset 0x370 contains 2 entries: Offset Info Type Sym.Value Sym. Name 0000001f 00000501 R_386_32 00000000 .rodata 00000024 00000902 R_386_PC32 00000000 printf
在.text段中有兩個(gè)relocation,其中之一就是printf函數(shù)的relcation。Offset指出當(dāng)relocation時(shí)要把 printf函數(shù)的入口地址貼到離.text段開(kāi)頭00000024處。
下面我們可以看一下連接過(guò)后的可執(zhí)行文件中的內(nèi)容:
代碼:
$:gcc hello.o
$:readelf -S a.out
There are 32 section headers, starting at offset 0xbc4:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 08048134 000134 000013 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 08048148 000148 000020 00 A 0 0 4
[ 3] .hash HASH 08048168 000168 00002c 04 A 4 0 4
[ 4] .dynsym DYNSYM 08048194 000194 000060 10 A 5 1 4
[ 5] .dynstr STRTAB 080481f4 0001f4 000060 00 A 0 0 1
[ 6] .gnu.version VERSYM 08048254 000254 00000c 02 A 4 0 2 [ 7] .gnu.version_r VERNEED 08048260 000260 000020 00 A 5 1 4 [ 8] .rel.dyn REL 08048280 000280 000008 08 A 4 0 4
[ 9] .rel.plt REL 08048288 000288 000010 08 A 4 11 4
[10] .init PROGBITS 08048298 000298 000017 00 AX 0 0 4
[11] .plt PROGBITS 080482b0 0002b0 000030 04 AX 0 0 4
[12] .text PROGBITS 080482e0 0002e0 0001b4 00 AX 0 0 16
[13] .fini PROGBITS 08048494 000494 00001a 00 AX 0 0 4
[14] .rodata PROGBITS 080484b0 0004b0 000016 00 A 0 0 4
[15] .eh_frame PROGBITS 080484c8 0004c8 000004 00 A 0 0 4
[16] .ctors PROGBITS 080494cc 0004cc 000008 00 WA 0 0 4
[17] .dtors PROGBITS 080494d4 0004d4 000008 00 WA 0 0 4
[18] .jcr PROGBITS 080494dc 0004dc 000004 00 WA 0 0 4
[19] .dynamic DYNAMIC 080494e0 0004e0 0000c8 08 WA 5 0 4
[20] .got PROGBITS 080495a8 0005a8 000004 04 WA 0 0 4
[21] .got.plt PROGBITS 080495ac 0005ac 000014 04 WA 0 0 4
[22] .data PROGBITS 080495c0 0005c0 00000c 00 WA 0 0 4
[23] .bss NOBITS 080495cc 0005cc 000004 00 WA 0 0 4
[24] .comment PROGBITS 00000000 0005cc 0001b2 00 0 0 1 [25] .debug_aranges PROGBITS 00000000 000780 000058 00 0 0 8 [26] .debug_info PROGBITS 00000000 0007d8 000164 00 0 0 1 [27] .debug_abbrev PROGBITS 00000000 00093c 000020 00 0 0 1 [28] .debug_line PROGBITS 00000000 00095c 00015a 00 0 0 1
[29] .shstrtab STRTAB 00000000 000ab6 00010c 00 0 0 1
[30] .symtab SYMTAB 00000000 0010c4 000510 10 31 56 4
[31] .strtab STRTAB 00000000 0015d4 000322 00 0 0 1
Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)
這里的段比目標(biāo)文件hello.o的段要多的多,這是因?yàn)檫@個(gè)程序需要elf的一個(gè)動(dòng)態(tài)連接庫(kù)libc.so.1。在這里需要簡(jiǎn)單的介紹一下內(nèi)核加載 elf可執(zhí)行文件。內(nèi)核先是把整個(gè)文件加載到用戶(hù)的虛擬內(nèi)存空間,如果程序是與動(dòng)態(tài)連接庫(kù)連接的,則程序中就會(huì)包含動(dòng)態(tài)連接器的名稱(chēng),可能是 /lib/elf/ld-linux.so.1。(動(dòng)態(tài)連接器本身也是一個(gè)動(dòng)態(tài)連接庫(kù))
在文件的尾部的一些段的Addr值是00000000,因?yàn)檫@些都是符號(hào)表,動(dòng)態(tài)連接器并不把這些段的內(nèi)容加載到內(nèi)存中。. interp段中只是儲(chǔ)存這一個(gè)ASCII的字符串,它就是動(dòng)態(tài)連接器的名字(路徑)。.hash, .dynsym, .dynstr這三個(gè)段是用于動(dòng)態(tài)連接器執(zhí)行relocation時(shí)的符號(hào)表。.hash是一個(gè)哈希表,可以讓我們很快的從.dynsym中找到所需的符號(hào)。
.plt段中儲(chǔ)存著我們調(diào)用動(dòng)態(tài)連接庫(kù)中的函數(shù)入口地址,在默認(rèn)狀態(tài)下,程序初始化時(shí),.plt中的指針并不是指向正確的函數(shù)入口地址的而是指向動(dòng)態(tài)連接器本身,當(dāng)你在程序中調(diào)用某個(gè)動(dòng)態(tài)連接庫(kù)中的函數(shù)時(shí),連接器會(huì)找到那個(gè)函數(shù)在動(dòng)態(tài)連接庫(kù)中的位置,再把這個(gè)位置連接到.plt段中。這樣做的好處是如果在程序中調(diào)用了很多動(dòng)態(tài)連接庫(kù)中的函數(shù),會(huì)花費(fèi)掉連接器很長(zhǎng)時(shí)間把每個(gè)函數(shù)的地址連接到.plt段中。所以就可以采用連接器只是把要用的函數(shù)地址連接進(jìn)去,以后要用的再連接。但是也可以設(shè)置環(huán)境變量LD_BIND_NOW=1讓連接器在程序執(zhí)行前把所有的函數(shù)地址都連接好,這主要是方便調(diào)試程序。
readelf工具還有很多選項(xiàng),具體內(nèi)容可以查看man手冊(cè)。在文章的開(kāi)頭就說(shuō)elf文件格式很方便運(yùn)用動(dòng)態(tài)連接技術(shù),下面我就寫(xiě)一個(gè)就簡(jiǎn)單的動(dòng)態(tài)連接庫(kù)的例子:
代碼:
$:cat Dyn_hello.c
int main(void)
{
? hi();
}
?
$:cat hi.c
#include <stdio.h>
hi()
{
? printf("Hello world\n");
}
兩個(gè)簡(jiǎn)單的文件,在mian函數(shù)中調(diào)用hi()函數(shù),下面并不是把兩個(gè)文件一起編譯,而是把hi.c編譯成動(dòng)態(tài)連接庫(kù)。(注意Dyn_hello.c中并沒(méi)有包含任何頭文件。)
代碼:
$:gcc -fPIC -c hi.c
$:gcc -shared -o libhi.so hi.o
現(xiàn)在在當(dāng)前目錄下有一個(gè)名字為libhi.so的文件,這就就是僅含有一個(gè)函數(shù)的動(dòng)態(tài)連接庫(kù)。
代碼:
$:gcc -c Dyn_hello.c
$:gcc -o Dyn_hello Dyn_hello.o -L. -lhi
在當(dāng)前目錄下有了一個(gè)Dyn_hello可執(zhí)行文件,現(xiàn)在就可以執(zhí)行它了。
代碼:
$:./Dyn_hello
./Dyn_hello: error while loading shared libraries: libhi.so: cannot open shared object file: No such file or directory
執(zhí)行不成功,這就表明了這是一個(gè)動(dòng)態(tài)連接的程序,連接器找不到libhi.so這個(gè)動(dòng)態(tài)連接庫(kù)。在命令行加上 LD_LIBRARY_PATH=...就行了。像這樣運(yùn)行:
代碼:
$:LD_LIBRARY_PATH=. ./Dyn_hello Hello world
指出當(dāng)前目錄是連接器的搜索目錄,就可以了。
Elf可執(zhí)行文件還有一個(gè)a.out很難實(shí)現(xiàn)的特點(diǎn),就是對(duì)dlopen()函數(shù)的支持,這個(gè)函數(shù)可以在程序中控制動(dòng)態(tài)的加載動(dòng)態(tài)連接庫(kù),看下面的一個(gè)小程序:
代碼:
$:cat Dl_hello.c
#include <dlfcn.h>
int main (int argc, char *argv[])
{
?? void (*hi) ();
?? void *m; if (argc > 2) exit (0);
?? m = dlopen (argv[1], RTLD_LAZY);
?? if (!m)
????? exit (0);
?? hi = dlsym (m, "hi");
?? if (hi)
?? {
????? (*hi) ();
??? }
??? dlclose (m);
}
用一下命令編譯:
代碼:
$:gcc -c Dl_hello.c
$:gcc -o Dl_hello Dl_hello.o -ldl
運(yùn)行Dl_hello程序加上動(dòng)態(tài)連接庫(kù)。
代碼:
$:./Dl_hello ./libhi.so Hello world
命令行成功的打印出了Hello world說(shuō)明我們的動(dòng)態(tài)連接庫(kù)運(yùn)用成功了。
在這篇文章中只是討論了elf可執(zhí)行文件的執(zhí)行原理,還有很多方面沒(méi)有涉及到,要深入了解elf你也許需要對(duì)動(dòng)態(tài)連接器hack一下,也要hack一下內(nèi)核加載程序的loader。但是我想對(duì)大多數(shù)人來(lái)說(shuō),這篇文章對(duì)elf的介紹已經(jīng)足夠讓你可以自己對(duì)elf在進(jìn)行比較深入的研究了。
標(biāo)簽: