查看Linux內(nèi)核代碼時(shí),經(jīng)常能看到一些編譯器選項(xiàng)如__attribute__((weak),起初不太了解,經(jīng)過(guò)查資料,算是對(duì)gcc的這個(gè)編譯屬性有了初步的認(rèn)識(shí),現(xiàn)在總結(jié)如下。
編譯器在編譯源程序時(shí),無(wú)論你是變量名、函數(shù)名,在它眼里,都是一個(gè)符號(hào)而已,用來(lái)表示一個(gè)地址。編譯器會(huì)將這些符號(hào)集中存放到一個(gè)叫符號(hào)表的 section 中。
程序編譯鏈接的基本過(guò)程總結(jié)起來(lái)其實(shí)很簡(jiǎn)單,大概分為如下三個(gè)階段:
- 編譯階段:編譯器以源文件為單位,將每一個(gè)源文件編譯為一個(gè).o后綴的目標(biāo)文件。每一個(gè)目標(biāo)文件由代碼段、數(shù)據(jù)段、符號(hào)表等組成。
- 鏈接階段:鏈接器將各個(gè)目標(biāo)文件組裝成一個(gè)大目標(biāo)文件。鏈接器將各個(gè)目標(biāo)文件中的代碼段組裝在一起,組成一個(gè)大的代碼段;各個(gè)數(shù)據(jù)段組裝在一起,組成一個(gè)大的數(shù)據(jù)段;各個(gè)符號(hào)表也會(huì)集中在一起,組成一個(gè)大的符號(hào)表。最后再將合并后的代碼段、數(shù)據(jù)段、符號(hào)表等組合成一個(gè)大的目標(biāo)文件。
- 重定位:由于鏈接階段各個(gè)目標(biāo)文件重新組裝,各個(gè)目標(biāo)文件中的變量和函數(shù)的地址都發(fā)生了變化,所以要重新修正這些函數(shù)及變量的地址,這個(gè)過(guò)程稱(chēng)為重定位。重定位結(jié)束后,才生成了可以在機(jī)器上運(yùn)行的可執(zhí)行程序。
一、weak屬性
attribute ((weak))表示為弱符號(hào)屬性,所謂的弱符號(hào)是針對(duì)于強(qiáng)符號(hào)來(lái)說(shuō)的,我們定義的全局已初始化變量及全局函數(shù)等都是屬于強(qiáng)符號(hào),在鏈接時(shí)如果有多個(gè)強(qiáng)符號(hào)就會(huì)報(bào)錯(cuò)誤;而弱符號(hào)主要指未初始化的全局變量或通過(guò)__attribute__((weak))來(lái)顯式聲明的變量或函數(shù)。
在日常編程過(guò)程中,我們可能會(huì)碰到一種符號(hào)重復(fù)定義的情況。如果多個(gè)目標(biāo)文件中含有相同名字的全局變量的定義,那么這些目標(biāo)文件鏈接的時(shí)候就會(huì)出現(xiàn)符號(hào)重復(fù)定義的錯(cuò)誤。比如在源文件date.c 和源文件weak_attr.c都定義了一個(gè)全局整型變量year,并且均已初始化,那么當(dāng)date.c和weak_attr.c鏈接時(shí)會(huì)報(bào)錯(cuò):
multiple definition of 'xxx'
重復(fù)定義的源碼文件如下:
/* 頭文件date.h */
#ifndef __DATE_H__
#define __DATE_H__
void currentYear();
#endif
/* 源文件date.c */
#include < stdio.h >
#include "date.h"
int year=2023;
void currentYear()
{
printf("This year is %d.\\n", year);
}
/* 源文件weak_attr.c */
#include < stdio.h >
#include "date.h"
int year=2022;
int main()
{
currentYear();
return 0;
}
gcc編譯輸出結(jié)果如下:
[root@localhost 119]# gcc -o weak_attr date.c weak_attr.c
/tmp/ccpmkhms.o:(.data+0x0): multiple definition of `year'
/tmp/ccsxbab2.o:(.data+0x0): first defined here
collect2: error: ld returned 1 exit status
那么該如何解決這個(gè)問(wèn)題呢?讓我們繼續(xù)往下看,下文會(huì)給出解決之道。
二、強(qiáng)符號(hào)弱符號(hào)
在程序中,無(wú)論是變量名,還是函數(shù)名,在編譯器的眼里,都只是一個(gè)符號(hào)而已。符號(hào)可以分為強(qiáng)符號(hào)和弱符號(hào)。
- 強(qiáng)符號(hào):函數(shù)名、初始化的全局變量名;
- 弱符號(hào):未初始化的全局變量名、 attribute _((weak)修飾的變量或函數(shù)
強(qiáng)符號(hào)和弱符號(hào)在解決程序編譯鏈接過(guò)程中,出現(xiàn)的多個(gè)同名變量、函數(shù)的沖突問(wèn)題非常有用。一般我們遵循下面三個(gè)規(guī)則:
- 一山不容二虎
- 強(qiáng)弱可以共處
- 體積大者為主
上面為方便記憶總結(jié)的3點(diǎn)原則,具體表述如下:
強(qiáng)弱符號(hào)總結(jié)規(guī)則如下:
規(guī)則1:不允許強(qiáng)符號(hào)被重復(fù)多次定義,但是強(qiáng)弱符號(hào)可以共存。
規(guī)則2:如果一個(gè)符號(hào)在某個(gè)目標(biāo)文件中是強(qiáng)符號(hào),但在其他文件中都是弱符號(hào),那么編譯時(shí)以強(qiáng)符號(hào)的值為準(zhǔn)。
規(guī)則3:如果一個(gè)符號(hào)在所有的目標(biāo)文件中都是弱符號(hào),那么選擇其中占用空間最大的一個(gè)。這個(gè)其實(shí)很好理解,編譯器不知道編程者的用意,選擇占用空間大的符號(hào)至少不會(huì)造成諸如溢出、越界等嚴(yán)重后果。
下面我們以一個(gè)例子說(shuō)明強(qiáng)弱符號(hào),如下:
#include < stdio.h >
extern int temp; // 非強(qiáng)符號(hào)也非弱符號(hào)
int weak; // 弱符號(hào)
int strong = 1; // 強(qiáng)符號(hào)
__attribute__((weak)) int weak_attr = 2; // 弱符號(hào)
int main()
{
//代碼
return 0;
}
在默認(rèn)的符號(hào)類(lèi)型情況下,強(qiáng)符號(hào)和弱符號(hào)是可以共存的,類(lèi)似于這樣:
/* 源文件test.c */
#include< stdio.h >
int year; /* 弱符號(hào) */
int year = 2023; /* 強(qiáng)符號(hào) */
int main()
{
printf("Current year is %d\\n",year);
return 0;
}
gcc編譯執(zhí)行輸出結(jié)果如下:
[root@localhost 119]# gcc -o test test.c
[root@localhost 119]# ./test
Current year is 2023
編譯不會(huì)報(bào)錯(cuò),在編譯時(shí)year的取值將會(huì)是2023。
這里我們回到本文最初的例子,我們將源文件weak_attr.c中的year=2022使用__attribute___((weak)修飾,則會(huì)將源文件weak_attr.c中的year=2022由強(qiáng)符號(hào)轉(zhuǎn)換為弱符號(hào),此時(shí),程序編譯鏈接則不會(huì)報(bào)錯(cuò),源文件以及編譯鏈接和執(zhí)行情況如下:
/* 頭文件date.h */
#ifndef __DATE_H__
#define __DATE_H__
void currentYear();
#endif
/* 源文件date.c */
#include < stdio.h >
#include "date.h"
int year=2023;
void currentYear()
{
printf("This year is %d.\\n", year);
}
/* 源文件weak_attr.c */
#include < stdio.h >
#include "date.h"
int __attribute__((weak)) year=2022;
int main()
{
printf("The value of year is : %d\\n.",year);
currentYear();
return 0;
}
gcc編譯輸出結(jié)果如下:
[root@localhost 119]# gcc -o weak_attr date.c weak_attr.c -g
[root@localhost 119]#
[root@localhost 119]# ./weak_attr
The value of year is : 2023
.This year is 2023.
由此可見(jiàn),當(dāng)不同源文件中存在定義同名變量的情況下,要想編譯不報(bào)錯(cuò),則可根據(jù)具體場(chǎng)景將強(qiáng)符號(hào)轉(zhuǎn)換為弱符號(hào),雖然這樣可能沒(méi)有太大意義,并且容易引發(fā)問(wèn)題,但是也不失為一種解決辦法。
但是使用__attribute__((weak))將強(qiáng)符號(hào)轉(zhuǎn)換為弱符號(hào)時(shí),卻不能在同一個(gè)文件中同時(shí)存在同名的強(qiáng)符號(hào),類(lèi)似于這樣:
/* 源文件test.c */
#include< stdio.h >
int __attribute__((weak)) year = 2022;
int year=2023;
int main()
{
printf("Current year is %d\\n",year);
return 0;
}
編譯器將報(bào)重復(fù)定義錯(cuò)誤。
[root@localhost 119]# gcc -o test test.c
test.c:4:5: error: redefinition of ‘year’
int year = 2023;
^~~~
test.c:3:27: note: previous definition of ‘year’ was here
int __attribute__((weak)) year = 2022;
編程時(shí),通常容易被忽略或者錯(cuò)誤認(rèn)識(shí)的一個(gè)點(diǎn)是: 全局變量不進(jìn)行初始化,編譯器在編譯時(shí)會(huì)自動(dòng)初始化為0 。如下即為編譯器在編譯期間自動(dòng)為未初始化的全局變量初始化為0的案例:
[root@localhost 119]# gcc -o test test.c -g
[root@localhost 119]# gdb ./test
GNU gdb (GDB) Red Hat Enterprise Linux 8.2-19.el8
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later < http://gnu.org/licenses/gpl.html >
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
< http://www.gnu.org/software/gdb/bugs/ >.
Find the GDB manual and other documentation resources online at:
< http://www.gnu.org/software/gdb/documentation/ >.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./test...done.
(gdb) list
1 #include< stdio.h >
2
3 //int __attribute__((weak)) year = 2022;
4 int year;
5
6 int main()
7 {
8 printf("Current year is %d\\n",year);
9 return 0;
10 }
(gdb)
Line number 11 out of range; test.c has 10 lines.
(gdb) print year
$1 = 0
(gdb)
大部分人都認(rèn)為 C 程序中的未初始化全局變量會(huì)在程序編譯的期間被默認(rèn)初始化為 0,因此不需要在程序中執(zhí)行初始化操作。這個(gè)觀點(diǎn)既正確又不完全正確。此話怎講,因?yàn)樵撚^點(diǎn)是有前提條件的,即 該全局變量在項(xiàng)目工程內(nèi)全局唯一時(shí),則編譯器在編譯時(shí)會(huì)自動(dòng)將該全局變量初始化為0 。否則,一旦該全局變量在項(xiàng)目工程內(nèi)不唯一,且在另一個(gè)文件內(nèi)有已被初始化的另一同名全局變量時(shí),則該變量的值為被初始化的全局變量的值,而非0。
請(qǐng)看如下案例,一個(gè)全局變量year在文件weak_attr.c中被定義并初始化為2023,而在文件date.c中被定義但沒(méi)有初始化,通過(guò)上文的討論可以知道,這并不會(huì)報(bào)錯(cuò),此時(shí)date.c文件中的全局變量year(弱符號(hào))被覆蓋,但是它的值并不會(huì)是預(yù)想中的被初始化為 0,而是weak_attr.c中初始化的值,這種情況下就可能造成一些問(wèn)題。
/* 源文件date.c */
#include < stdio.h >
#include "date.h"
int year=2023;
void currentYear()
{
printf("This year is %d.\\n", year);
}
/* 源文件weak_attr.c */
#include < stdio.h >
#include "date.h"
int year;
int main()
{
printf("The value of year is : %d\\n.",year);
currentYear();
return 0;
}
gcc編譯調(diào)試輸出如下:
[root@localhost 119]# gdb ./weak_attr
GNU gdb (GDB) Red Hat Enterprise Linux 8.2-19.el8
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later < http://gnu.org/licenses/gpl.html >
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
< http://www.gnu.org/software/gdb/bugs/ >.
Find the GDB manual and other documentation resources online at:
< http://www.gnu.org/software/gdb/documentation/ >.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./weak_attr...done.
(gdb) l
1 #include < stdio.h >
2 #include "date.h"
3
4 int year;
5
6 int main()
7 {
8 printf("The value of year is : %d\\n.",year);
9 currentYear();
10 return 0;
(gdb)
11 }
(gdb) print year
$1 = 2023
(gdb)
執(zhí)行程序輸出結(jié)果如下:
[root@localhost 119]# ./weak_attr
The value of year is : 2023
.This year is 2023.
從上述結(jié)果可看出,year的值被初始化為2023,而并非為0。
當(dāng)然,這并一定就說(shuō)明所有全局變量在定義時(shí)就應(yīng)該初始化為 0,畢竟未初始化的全局變量被放置在 bss 段,對(duì)于某些數(shù)據(jù)結(jié)構(gòu)將會(huì)節(jié)省大量空間,這是有意義的。只是我們?cè)谒伎际欠裥枰獙?duì)全局變量進(jìn)行初始化的時(shí)候需要將上面可能出現(xiàn)的問(wèn)題考慮進(jìn)去,根據(jù)實(shí)際的場(chǎng)景選擇合適的方案。
三、函數(shù)的強(qiáng)符號(hào)和弱符號(hào)
鏈接器對(duì)于同名變量沖突的處理遵循上面的強(qiáng)弱規(guī)則,對(duì)于同名函數(shù)的沖突,也遵循相同的規(guī)則。函數(shù)名本身就是一個(gè)強(qiáng)符號(hào),在一個(gè)工程中定義兩個(gè)同名的函數(shù),編譯時(shí)肯定會(huì)報(bào)重定義錯(cuò)誤。但我們可以通過(guò) weak 屬性聲明,將其中一個(gè)函數(shù)轉(zhuǎn)換為弱符號(hào)。
/* function.c */
int num __attribute__((weak)) = 1;
void __attribute__((weak)) func(void)
{
printf("func:num = %d\\n", num);
}
/* main.c */
#include < stdio.h >
int num = 4;
void func(void)
{
printf("I am a strong symbol!\\n");
}
int main(void)
{
printf("main:num = %d\\n", num);
func();
return 0;
}
編譯程序,可以看到程序運(yùn)行結(jié)果如下:
[root@localhost 130]# gcc -o main main.c function.c
[root@localhost 130]#
[root@localhost 130]# ./main
main:num = 4
I am a strong symbol!
在這個(gè)程序示例中,我們?cè)?main.c 中重新定義了一個(gè)同名的 func 函數(shù),然后將 function.c 文件中的 func() 函數(shù),通過(guò) weak 屬性聲明轉(zhuǎn)換為一個(gè)弱符號(hào)。鏈接器在鏈接時(shí)會(huì)選擇 main.c 中的強(qiáng)符號(hào),所以我們?cè)?main 函數(shù)中調(diào)用 func() 時(shí),實(shí)際上調(diào)用的是 main.c 文件里的 func() 函數(shù)。
四、弱符號(hào)的作用
在一個(gè)源文件中引用一個(gè)變量或函數(shù),當(dāng)我們僅聲明而未定義時(shí),一般編譯是可以通過(guò)的。因?yàn)榫幾g是以文件為單位的,編譯器會(huì)將一個(gè)個(gè)源文件首先編譯為 .o 目標(biāo)文件。編譯器只要能看到函數(shù)或變量的聲明,就會(huì)認(rèn)為這個(gè)變量或函數(shù)的定義可能會(huì)在其它的文件中,所以不會(huì)報(bào)錯(cuò)。甚至如果你沒(méi)有包含頭文件,連個(gè)聲明也沒(méi)有,編譯器也不會(huì)報(bào)錯(cuò),頂多就是給你一個(gè)警告信息。但鏈接階段是要報(bào)錯(cuò)的,鏈接器在各個(gè)目標(biāo)文件、庫(kù)中都找不到這個(gè)變量或函數(shù)的定義,一般就會(huì)報(bào)未定義錯(cuò)誤。
當(dāng)函數(shù)被聲明為一個(gè)弱符號(hào)時(shí),會(huì)有一個(gè)特別的地方: 當(dāng)鏈接器找不到這個(gè)函數(shù)的定義時(shí),也不會(huì)報(bào)錯(cuò) 。編譯器會(huì)將這個(gè)函數(shù)名,即弱符號(hào),設(shè)置為0或一個(gè)特殊的值。只有當(dāng)程序運(yùn)行時(shí),調(diào)用到這個(gè)函數(shù),跳轉(zhuǎn)到0地址或一個(gè)特殊的地址才會(huì)報(bào)錯(cuò)。
/* function.c */
int num __attribute__((weak)) = 1;
/* main.c */
int num = 5;
void __attribute__((weak)) func(void);
int main(void)
{
printf("main:num = %d\\n", num);
func();
return 0;
}
編譯程序,可以看到程序運(yùn)行結(jié)果如下:
[root@localhost 130]# gcc -o main main.c function.c
[root@localhost 130]#
[root@localhost 130]# ./main
main:num = 5
Segmentation fault (core dumped)
在這個(gè)示例程序中,我們沒(méi)有定義 func() 函數(shù),僅僅是在 main.c 里作了一個(gè)聲明,并將其聲明為一個(gè)弱符號(hào)。編譯這個(gè)工程,你會(huì)發(fā)現(xiàn)是可以編譯通過(guò)的,只是到了程序運(yùn)行時(shí)才會(huì)出錯(cuò)。
為防止函數(shù)執(zhí)行出錯(cuò),可以在執(zhí)行函數(shù)之前,先做一個(gè)判斷,即判斷函數(shù)名的地址是不是0,再?zèng)Q定是否調(diào)用、運(yùn)行,如果是0,則不進(jìn)行調(diào)用。這樣就可以避免段錯(cuò)誤了,示例代碼如下:
/* function.c */
int a __attribute__((weak)) = 1;
/* main.c */
#include < stdio.h >
int num = 5;
void __attribute__((weak)) func(void);
int main(void)
{
printf("main:num = %d\\n", num);
if(func)
{
func();
}
return 0;
}
編譯程序,可以看到程序運(yùn)行結(jié)果如下:
[root@localhost 130]# gcc -o main main.c function.c
[root@localhost 130]# ./main
main:num = 5
實(shí)際上函數(shù)名的本質(zhì)就是一個(gè)地址,在調(diào)用 func 之前,我們先判斷其是否為0,為0的話就不調(diào)用該函數(shù),直接跳過(guò)。通過(guò)這樣的設(shè)計(jì),即使這個(gè) func() 函數(shù)沒(méi)有定義,我們整個(gè)工程也能正常的編譯、鏈接和運(yùn)行。
弱符號(hào)的這個(gè)特性,在庫(kù)函數(shù)中應(yīng)用很廣泛。比如你在開(kāi)發(fā)一個(gè)庫(kù),基礎(chǔ)的功能已經(jīng)實(shí)現(xiàn),有些高級(jí)的功能還沒(méi)實(shí)現(xiàn),那你可以將這些函數(shù)通過(guò) weak 屬性聲明,轉(zhuǎn)換為一個(gè)弱符號(hào)。通過(guò)這樣設(shè)置,即使函數(shù)還沒(méi)有定義,我們?cè)趹?yīng)用程序中只要做一個(gè)非0的判斷就可以了,并不影響我們程序的運(yùn)行。等以后你發(fā)布新的庫(kù)版本,實(shí)現(xiàn)了這些高級(jí)功能,應(yīng)用程序也不需要任何修改,直接運(yùn)行就可以調(diào)用這些高級(jí)功能。
弱符號(hào)還有一個(gè)好處,如果我們對(duì)庫(kù)函數(shù)的實(shí)現(xiàn)不滿意,我們可以自定義與庫(kù)函數(shù)同名的函數(shù),實(shí)現(xiàn)更好的功能。比如我們 C 標(biāo)準(zhǔn)庫(kù)中定義的 gets() 函數(shù),就存在漏洞,常常成為黑客堆棧溢出攻擊的靶子。
int main(void)
{
char a[5];
gets(a);
puts(a);
return 0;
}
編譯時(shí)會(huì)出現(xiàn)一個(gè)warning,建議我們不要使用gets函數(shù)了。
[root@localhost 130]# gcc -o test test.c
test.c: In function ‘main’:
test.c:7:4: warning: implicit declaration of function ‘gets’; did you mean ‘fgets’? [-Wimplicit-function-declaration]
gets(a);
^~~~
fgets
/tmp/cckNkApE.o: In function `main':
test.c:(.text+0x15): warning: the `gets' function is dangerous and should not be used.
我們暫時(shí)不管他,先直接運(yùn)行看結(jié)果:
[root@localhost 130]# ./test
hello,my name is localhost, nice to meet you
hello,my name is localhost, nice to meet you
Segmentation fault (core dumped)
C 標(biāo)準(zhǔn)定義的庫(kù)函數(shù) gets() 主要用于輸入字符串,它的一個(gè)bug就是使用回車(chē)符來(lái)判斷用戶(hù)輸入結(jié)束標(biāo)志。這樣的設(shè)計(jì)很容易造成堆棧溢出。比如上面的程序,我們定義一個(gè)長(zhǎng)度為5的字符數(shù)組用來(lái)存儲(chǔ)用戶(hù)輸入的字符串,當(dāng)我們輸入一個(gè)長(zhǎng)度大于5的字符串時(shí),就會(huì)發(fā)生內(nèi)存錯(cuò)誤。
接著我們定義一個(gè)跟 gets() 相同類(lèi)型的同名函數(shù),并在 main 函數(shù)中直接調(diào)用,代碼如下。
/* test.c */
#include < stdio.h >
char * gets (char * str)
{
printf("my custom function!\\n");
return (char *)0;
}
int main(void)
{
char a[5];
gets(a);
puts(a);
return 0;
}
編譯運(yùn)行,程序執(zhí)行結(jié)果如下:
[root@localhost 130]# gcc -o test test.c
[root@localhost 130]# ./test
my custom function!
通過(guò)運(yùn)行結(jié)果,我們可以看到,雖然我們定義了跟 C 標(biāo)準(zhǔn)庫(kù)函數(shù)同名的 gets() 函數(shù),但編譯是可以通過(guò)的。程序運(yùn)行時(shí)調(diào)用 gets() 函數(shù)時(shí),就會(huì)跳轉(zhuǎn)到我們自定義的 gets() 函數(shù)中運(yùn)行,從而實(shí)現(xiàn)了漏洞攻擊。
評(píng)論