電腦科學基礎知識(四): 動態庫和位置無關程式碼
一、前言
本文主要描述了動態庫以及和動態庫有緊密聯絡的位置無關程式碼的相關資訊。首先介紹了動態庫和位置無關程式碼的源由,瞭解這些背景知識有助於理解和學習動態庫。隨後,我們通過加-fPIC和不加這個編譯選項分別編譯出兩個relocatable object file,看看編譯器是如何生成位置無關程式碼的。最後,我們自己動手編寫一個簡單的動態庫,並解析了一些symbol Visibility、動態符號表等一些相關基本概念。
本文中的描述是基於ARM MCU,GNU/linux平臺而言的,本文是個人對動態庫的理解,如果有錯誤,請及時指出。
二、背景介紹
位置無關程式碼實際上是和動態庫概念緊密聯絡在一起的,本章首先描述為何會提出動態庫的概念,然後解釋動態庫為何需要編譯成PIC的程式碼。
1、為何會提出動態庫的概念?
引入靜態庫後,解決了一些問題,但是仍然存在下面的弊端:
(1)任何對靜態庫的升級都需要rebuild(或者叫做relink)的過程
(2)通用的函式(例如標準IO函式scanf和printf)存在於各個靜態連結的程式中,導致編譯後的靜態可執行程式的size比較大,在各個可執行程式中,這些通用的函式程式碼是重複的,佔用了磁碟和記憶體資源
正因為如此,動態庫和動態連結的概念被提出來來解決這些問題。動態庫也是一種ELF格式的物件檔案,在執行的時候,它可以被載入到任何的地址執行。
2、動態庫為何需要編譯成PIC的程式碼?
無論是動態庫還是靜態庫,其本質都是程式碼共享。對於靜態庫,其程式碼以及資料都是在各個靜態連結的可執行檔案中有一份copy,所有符號的地址已經確定,因此在loading的時候,OS會比較輕鬆。不過這種程式碼共享無法在run time的時候共享程式碼,從而導致了資源的浪費。當然,它的好處就是簡單、速度快(無需dynamic linker來重定位符號)。對於靜態編譯,static linker將多個編譯單元(.o檔案和庫檔案)整合成一個模組,因此,進入run time,實際上只有一個執行模組。對於動態連結,在run time的時候,除了可執行檔案這個模組,該可執行檔案所依賴的各個動態庫也是一個個的執行模組,這時候,可執行檔案呼叫動態庫的符號實際上是就是需要引用其他執行模組的符號了。對於可執行檔案而言,loader將其載入到哪個地址並不關鍵,反正每個程式都有自己獨一無二的地址空間,可執行檔案可以mapping到各自virtual memory space的相同地址也無妨,不過對於動態庫模組而言,就有些麻煩了。如果我們不將動態庫編譯成PIC的也就是意味著loader一定要把動態庫載入到某個特定的地址(該地址編譯的時候就確定了)上它才可以正確的執行。假設我們有A B C D四個動態庫,假設程式P1依賴A B兩個動態庫,P2依賴C D兩個動態庫,那麼A B和C D的動態庫的載入地址有重疊也沒有關係,P1和P2可以同時執行。但是如果有一個新的程式P3依賴A B C D四個動態庫,那麼前面為動態庫分配的載入地址就不能正常工作了。當然,重新為這四個動態庫分配load address(讓地址不重疊)也是ok的,但是這樣一來,P1雖然沒有使用C D這兩個動態庫,但是P1的地址空間還是要保留C D動態庫的那段地址,對於地址這樣寶貴資源,這麼浪費簡直是暴殄天物。更重要的是:這樣的機制實際上對程式虛擬地址的管理就變得非常複雜了,假設A B C D是分配了一段連續的地址,如果C動態庫更新了,size變大了,原本分配的地址空間不夠了,怎麼辦?我們必須再尋找一個新的地址段來載入C動態庫。如果系統只有四個動態庫起始還是OK的,如果動態庫非常非常多……怎麼辦?更糟的是:不同的系統使用不同的動態庫,管理起來更令人頭痛
最好的方法就是將動態庫編譯成PIC(Position Independent Code),也就是說動態庫可以被載入到任何地址並正確執行。
三、動手實踐:觀察PIC的.o檔案的反彙編結果
1、原始碼foo.c
#include <stdio.h>
int xxx = 0x1234;
int yyy;
int foo(void)
{
yyy = 0x5678;
printf("xxx=%x yyy=%x\n", xxx, yyy);
return xxx;
}
2、觀察foo.o檔案中的符號定位資訊
使用arm-linux-gcc –c foo.c將source code編譯成relocatable file。我們來看看正文段中的relocation資訊:
00000030 00000e1c R_ARM_CALL 00000000 printf
00000044 00000f02 R_ARM_ABS32 00000004 yyy
0000004c 00000c02 R_ARM_ABS32 00000000 xxx
R_ARM_ABS32是一種ARM平臺上的absolute 32-bit relocation,在32 bit的ARM平臺上,這種重定位的方式是沒有任何約束的,可以將地址重定位到4G地址空間的任何位置。具體實現方式需要參考反編譯的彙編程式碼,我們來看看彙編程式碼是如何訪問yyy這個資料的:
……
8: e59f2034 ldr r2, [pc, #52] ; 44 <.text+0x44>
c: e59f3034 ldr r3, [pc, #52] ; 48 <.text+0x48>
10: e5823000 str r3, [r2]
……
44: 00000000 .word 0x00000000
48: 00005678 .word 0x00005678
具體做法非常的簡單,在這段程式碼的後面(也是.text section的一部分)給出一個32-bit的跳板memory(上面黑色加粗的那一行),位於<.text+0x44>,這個memory用於儲存yyy符號的執行地址。由於同在一個正文段,因此它們之間的offset是確定的,使用“ldr r2, [pc, #52] ”這樣的PC-relative的訪問指令可以訪問到yyy變數的地址,通過“str r3, [r2]”可以將yyy變數的內容儲存到r3中。
下面我們我們再看看函式符號的訪問。R_ARM_CALL這種型別的重定位資訊主要用於函式呼叫的(對應的ARM指令就是BL和BLX),實現也很簡單,如下:
……
30: ebfffffe bl 0……
BL指令是一個PC-relative指令,會將控制權交給相對於當前PC值的一個地址上去(同時設定lr暫存器),bl這條指令的0~23個bit(用imm24表示))用來表示相對與PC的偏移地址,最終跳轉到的地址是PC+(imm24在低位新增00b,然後做符號擴充套件),也就是正負32M的區域(注意:BL不能任意跳轉4G範圍的地址空間)。之所以新增兩個0是因為offset地址總是4位元組對齊的。
對於靜態連結,很簡單,雖然那些重定位資訊在正文段,但是沒有關係,在程式loading之前,static linker可以修改正文段的內容。
3、編譯PIC的.o檔案並觀察
編譯成位置無關程式碼也就意味著這段程式碼多半是動態庫的一部分,需要動態載入到一個編譯時候未知的地址上。也就是說上文中使用的方法已經不行了,編譯時候符號的地址還是不確定的,因此static linker無法將地址填入到.text section中。在loading的時候,雖然知道了符號runtime address,但是正文段是read only的,也無法修改。怎麼辦呢?我們來一起看看程式如何實現。
使用arm-linux-gcc -fPIC–c foo.c將source code編譯成relocatable file。我們來看看正文段中的relocation資訊:
Relocation section '.rel.text' at offset 0x4e0 contains 5 entries:
Offset Info Type Sym.Value Sym. Name
00000048 00000f1b R_ARM_PLT32 00000000 printf
00000064 00001019 R_ARM_BASE_PREL 00000000 _GLOBAL_OFFSET_TABLE_
00000068 0000111a R_ARM_GOT_BREL 00000004 yyy
00000070 00000d1a R_ARM_GOT_BREL 00000000 xxx
我們首先看看_GLOBAL_OFFSET_TABLE_這個符號,看起來和傳說中的GOT(Global Offset Table)有關。那麼什麼是GOT呢?它有什麼作用呢?我們先回到c程式碼,思考一下對xxx符號的訪問。這時候,我們能確定xxx的runtime address嗎?當然不能,離loading還遠著呢,這時候我們能確定訪問xxx的程式碼(.text section中)和xxx符號(.data section)之間offset嗎?也不能,因為還有多個.o檔案最後被link成一個動態庫。怎麼辦?我們必須藉助一個橋樑來讓資料訪問變得Position Independent,這個橋樑就是GOT(Global Offset Table)。當然GOT必須是可讀可寫的,因為後續在run time的時候還要修改其內容。_GLOBAL_OFFSET_TABLE_就是定義了GOT在memory中的位置。因此64那個位置的重定位資訊和GOT相關,R_ARM_BASE_PREL這個relocation type則說明這個重定位資訊說明該位置儲存了GOT offset。由於目前還是.o檔案,還沒有確定最後GOT資訊,因此需要這個relocation的資訊,一旦完成動態庫的編譯,這個relocation entry就不需要了。
R_ARM_GOT_BREL這個type說明這個重定位資訊是一個描述GOT entry和GOT起始位置的offset。例如:yyy這個符號還需要relocation,那麼它的relocation位於正文段offset是0x68的位置,其內容儲存了yyy符號在GOT entry中的地址和GOT起始位置的偏移。OK,有了這些鋪墊,可以看看程式對yyy這個資料是如何訪問的:
……
c: e59f4050 ldr r4, [pc, #80] ; 64 <.text+0x64>
10: e08f4004 add r4, pc, r4 ---------------獲得GOT的起始位置的地址
14: e59f304c ldr r3, [pc, #76] ; 68 <.text+0x68> -----獲得yyy符號在GOT中的offset
18: e7942003 ldr r2, [r4, r3] --------------獲得yyy符號的runtime address
1c: e59f3048 ldr r3, [pc, #72] ; 6c <.text+0x6c>
20: e5823000 str r3, [r2] ---------------設定yyy符號的內容
……
64: 0000004c .word 0x0000004c-----GOT offset
68: 00000000 .word 0x00000000-----yyy的地址在GOT中的偏移
6c: 00005678 .word 0x00005678
由此可見,PIC的程式碼對全域性資料的訪問都是通過GOT來完成的,從而做到了位置無關。
四、動手實踐:觀察動態庫的反彙編結果
1、如何生成動態庫?
我們準備動手做一個動態庫了,先看source code,一如既往的簡單(注意:我們不建議匯出動態庫中的資料符號,這裡主要是為了描述動態庫的概念而這麼做的):
int xxx = 0x1234;
int yyy;
int foo(void)
{
yyy = 0x5678;
return xxx;
}
通過下面的命令可以編譯出一個libfoo的動態庫:
arm-linux-gcc -shared -fPIC -o libfoo.so foo.c
-shared告知gcc生成share object檔案,而-fPIC則告訴gcc請生成位置無關程式碼。
2、觀察符號表的變化
我們在relocatable object中已經對符號表進行了描述:對靜態編譯的程式而言,.o檔案中的符號表一是要對外宣稱自己定義了哪些符號,另外一個是向外宣佈自己引用了哪些符號,需要其他模組來支援。有了這些資訊,static linker才能整合各個relocatable object file中的資源,互通有無,最後融合成一個靜態的可執行程式。因此,實際上,對於靜態的可執行程式,在載入執行的時候,其符號表已經沒有任何意義了(不過可以方便debug),對於CPU而言,其執行就是要知道地址就OK了(靜態編譯程式所有的符號都已經定位了),符號什麼的它不關心,因此,實際上符號表可以刪除。如果你願意,你可以通過strip命令來進行實驗,看看tripped和not stripped的elf檔案有什麼不同。
然而,電腦科學的發展是不斷前進的,當有了動態庫之後,符號表會怎樣呢?我們自己可以動手生成一個動態連結的可執行程式或者動態庫並觀察其中的符號表資訊(恰好上一節已經生成一個libfoo.so,就它吧)。通過readelf工具,我們可以看到,動態連結的程式中有兩個符號表,一個是大家之前就熟悉的.symtab section(我們稱之符號表),另外一個就是.dynsym section(動態符號表)。這兩個符號表都有自己對應的string table,分別是.strtab和.dynstr section。
.symtab section我們前面的文章都有描述,為何又增加了一個.dynsym section呢?我們先假設我們編譯出來的動態庫只有一個符號表,那麼當使用strip命令刪除符號表以及對應的字串表之後會怎樣?當其他程式呼叫該動態庫提供的介面API函式的時候,dynamic linker還能找到對應的API函式符號嗎?當然不行,符號表都刪除了還想怎樣。靜態連結的程式之所以可以strip掉符號表以及對應的字串表那是因為程式中所有符號都已經塵埃落定(所有符號已經重定位),因此strip後也毫無壓力,但是動態連結的情況下,程式中的沒有定位的符號以及動態庫中宣稱的符號都需要有一個特別的符號表(是正常符號表的子集)來儲存動態連結符號的資訊,這個表就是動態連線符號表(.dynsym section)。
OK,最後總結一下:符號表(.symtab section)是指導static linker工作的,執行的時候可以不需要。動態符號表(.dynsym section)是給dynamic linker用的,程式(或者動態庫)執行的時候,dynamic linker用動態符號表的資訊來定位符號。
3、Binding Property和Symbol Visibility
我們在講述relocatable object file的時候已經給出了binding屬性(binding property)的解釋。一個符號可能有global、local和weak三種binding property。這個binding property主要是被static linker用來進行.o之間的符號解析(symbol resolution)的。Bind屬性之外還有一個屬性我們一直沒有描述(通過readelf觀察符號表的時候,該屬性對應列的名字是Vis的那個),我們稱之Symbol Visibility或者符號的可見性。之所以前面的文章中沒有描述主要是因為Symbol visibility是和動態庫以及動態連結相關的。
當引入動態連線和動態庫的概念之後,程式碼和資料的共享會變得複雜一些。和binding property不一樣,Symbol Visibility是針對執行模組(動態連結的可執行程式或者動態庫)之間的相互引用。例如我們有A.o B.o C.o三個編譯模組,static linker將這三個.o檔案link成一個libABC.so檔案。A.o模組要呼叫B.o中的一個函式bb,那麼bb函式就一定需要是一個GLOBAL型別的,但是bb函式並不是動態庫libABC.so的介面API(或者稱之export symbol),也就是說,為了更好的封裝性,我們希望bb這個函式對外不可見,dynamic linker看不到這個符號,bb不參與動態符號解析。如果動態庫匯出所有的符號,那麼,在動態連結的時候,符號衝突的可能性就非常的大,特別是對於那些大型專案,可能該專案涉及的每個動態庫都是由不同team負責的。除了模組的封裝性之外,Symbol Visibility也是和程式的效能有關。如果匯出太多的符號,除了佔用更多的記憶體,還意味著增加loading time和dynamic linking time。
看,不控制Symbol Visibility的危害還是很大D,這時候閱讀本文的你估計一定會問:那麼控制Symbol Visibility哪家強呢?我推薦使用大殺器static關鍵字,簡單,實用,人人會。給function或者全域性變數加上static關鍵字,別說是對dynamic linker(執行模組之間的引用)進行了限制,就是static linker(.o 檔案之間的引用)也是拿他毫無辦法。當然,缺點也很明顯:不能在動態庫的多個.o之間共享。在這種場景下,我們需要求助其他方法了,對於gcc,我們可以用下面的方法:
符號型別 符號名字 __attribute__ ((visibility ("xxx")));
其中xxx指明瞭該符號的Symbol Visibility屬性,Symbol Visibility屬性可以設定為:
(1)DEFAULT(雖然命名是default,但是有些public的味道)。該屬性的符號被匯出,該符號可以被其他執行模組訪問
(2)PROTECTED。同DEFAULT,不過該符號不能被overridden。也就是說,如果一個動態庫中的符號是PROTECTED,那麼動態庫中的程式碼訪問該符號是享有優先權的,即便其他的執行模組定義了同名的符號。
(3)HIDDEN。HIDDEN的符號不會被匯出,不參與動態連結。
(4)INTERNAL。其他執行模組不能訪問該型別的符號。
回到上一節描述的這個source code,其中有三個符號:xxx、yyy和foo,都是被匯出的,可以被其他的模組呼叫。如果你有興趣,可以自己試著控制符號的visibility,看看效果如何。
4、動態庫檔案的載入
libfoo這個shared object elf檔案的載入是根據Program header進行的。在ELF file header中可以看到該動態庫共計4個program header,如下:
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x00000000 0x00000000 0x005c0 0x005c0 R E 0x8000
LOAD 0x0005c0 0x000085c0 0x000085c0 0x00118 0x00120 RW 0x8000
DYNAMIC 0x0005cc 0x000085cc 0x000085cc 0x000e0 0x000e0 RW 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
帶有LOAD標記的那些program header entry會被mapping到程式地址空間上去。第一項是code segment,由於動態庫的程式碼是PIC的,因此其VirtAddr和PhysAddr都是0,表示可以執行在任意地址上。第二項是data segment,在實際中,動態庫的code和data segment都是連續載入的,因此,如果code segment的run time地址是0的話,那麼data segment的地址應該是0x5c0,不過由於code segment是0x8000對齊的,因此data segment的地址被設定為0x85c0。當然,如果實際該動態庫被載入到了程式的X虛擬地址上的話,data segment的runtime地址應該是X + 0x85c0。對於動態庫而言,其code segment可以被多個程式共享,也就是說,雖然code segment被載入到不同的程式的不同的虛擬地址空間,但是其實體地址是一樣的,只不過各個程式設定自己的page table就OK了。對於code segment,各個程式都有自己的副本,不可能共享的。
沒有LOAD標記,這說明第三項和第四項(DYNAMIC這個entry下一節描述)都是和程式載入無關的(不佔用程式虛擬地址空間)。GNU_STACK是用來告訴作業系統,當載入ELF檔案的時候,如果控制stack的屬性。這是和系統安全相關(通過stack來攻擊系統),我們在relocatable object file的時候已經描述,這裡略過(https://wiki.gentoo.org/wiki/Hardened/GNU_stack_quickstart中有更詳細的資訊)。
5、如何找到動態連結的資訊
和靜態連結的可執行序程式相比,DYNAMIC那個program header entry是動態庫檔案特有的。既然是動態庫,當然要參與動態連結的過程,因此動態庫的ELF檔案需要提供一些dynamic linking資訊給OS以及dynamic linker,DYNAMIC那個program header entry就是起這個作用的。dynamic segment只包含了一個section,名字是.dynamic。需要注意的是.dynamic section也是data segment的一部分被載入到了程式的地址空間中。下面我們仔細看看libfoo.so的Dynamic section的內容:
Dynamic section at offset 0x5cc contains 24 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libc.so.6]
0x0000000c (INIT) 0x460
0x0000000d (FINI) 0x5ac
0x00000019 (INIT_ARRAY) 0x85c0
0x0000001b (INIT_ARRAYSZ) 4 (bytes)
0x0000001a (FINI_ARRAY) 0x85c4
0x0000001c (FINI_ARRAYSZ) 4 (bytes)
……
我們先不著急看具體的各個項次的含義,我們先看看section table中對.dynamic的描述:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
……
[16] .dynamic DYNAMIC 000085cc 0005cc 0000e0 08 WA 3 0 4
由此可知,.dynamic section是有Entry size的,也就是說,這個section中的內容是按照8個byte形成一個個的entry,下面的這個Elf32_Dyn(對於64bit的CPU,對應是Elf64_Dyn)資料結構可以解析這8個bytes:
typedef struct {
Elf32_Sword d_tag; /* Dynamic entry type */
union {
Elf32_Word d_val; /* Integer value */
Elf32_Addr d_ptr; /* Address value */
} d_un;
} Elf32_Dyn;
d_tag定義dynamic entry的型別,而根據tag的不同,附加資料d_un可能是一個整數型別d_val,其含義和具體的tag相關,或者附加資料是一個虛擬地址d_ptr。瞭解了這些資訊後,我們可以來解析.dynamic section的具體內容了。
dynamic tag是NEEDED這個entry標識libfoo這個動態庫依賴的object檔案。ldd工具可以列印出給定程式或者動態庫的share library的依賴關係,本質上ldd就是應用了NEEDED這個tag資訊。對於libfoo.so這個動態庫,它會依賴libc.so.6這個動態庫,也就是c庫了。不過,你可能會奇怪,我們c程式碼沒有引用任何的c庫函式啊,怎麼會依賴c庫呢?其實這和靜態連結的hello world程式類似,我們在講靜態連結的時候已經描述了,你可以在build libfoo.so的時候加上-v的選項,這時候你可以從不斷滾動的螢幕資訊中找到答案:你的c程式碼不是一個人在戰鬥。你可以可以從.text中看到一些端倪,例如.text中有一個call_gmon_start的函式,這個函式本來就不是我們的c程式碼定義的符號,我們的c程式碼只定義了foo函式以及xxx、yyy這兩個變數符號。本來以為在.text中只有foo的定義,call_gmon_start是從那裡冒出來的呢?實際上這個符號定義在crti.o中(在最後生成libfoo.so的動態庫的時候,有若干個crt*.o參與其中)。libfoo.so定義了call_gmon_start這個函式,那麼什麼時候呼叫呢?這又回到了linux下動態庫的結構這個問題上:雖然動態庫定義了一些符號(函式或者全域性變數),但是,我們希望在呼叫這些函式或者訪問這些變數之前,先執行一些初始化的程式碼(這發生在動態庫載入的時候,dlopen的時候,由dynamic linker負責)。這些初始化程式碼被放到一些特殊的section(例如.init),libfoo.so的.init section的反彙編結果如下:
00000460 <_init>:
460: e52de004 str lr, [sp, #-4]!
464: e24dd004 sub sp, sp, #4 ; 0x4
468: eb000009 bl 494 -----以上來自crti.o這裡可以存放動態庫自己定義的初始化函式,當然我們這麼簡單的動態庫當然沒有。
46c: e28dd004 add sp, sp, #4 ; 0x4------以下來自crtn.o
470: e8bd8000 ldmia sp!, {pc}
INIT(對應.init section)到FINI_ARRAYSZ這些entry都是和該動態庫的初始化和退出函式相關的。當dynamic linker open這個動態庫的時候(dlopen)會執行初始化函式,當dynamic linker close這個動態庫的時候(dlclose)會執行退出函式。還有很多dynamic tag,這裡主要關注結構,暫且略過,一言以蔽之,dynamic linker可以通過.dynamic section找到所有它需要的動態連結資訊。
6、動態庫中訪問全域性變數
我們來看看foo中如何訪問yyy這個符號的。yyy的重定位資訊如下(.rel.dyn section中):
000086bc 00000815 R_ARM_GLOB_DAT 000086dc yyy
符號表中可以查到GOT的位置:
56: 000086ac 0 OBJECT LOCAL HIDDEN ABS _GLOBAL_OFFSET_TABLE_
當然0x86ac是一個offset,並不是run time address,畢竟只有loading後才知道其具體的地址資訊。如果該動態庫被loading到address_libfoo,那麼GOT實際應該位於address_libfoo+0x86ac。而yyy符號的地址在address_libfoo+0x86bc,dynamic linker會在適當的時間把真實的yyy符號的地址寫入到這個位置的。由此可見,在offset是0x000086bc(GOT中的某個entry)的位置上儲存了yyy符號的重定位資訊。
……
568: e59f202c ldr r2, [pc, #44] ; 59c <.text+0x108> ---獲取GOT到當前指令的偏移
56c: e08f2002 add r2, pc, r2 --------------獲取GOT的絕對地址
570: e59f3028 ldr r3, [pc, #40] ; 5a0 <.text+0x10c> ---獲取yyy在GOT中的偏移
574: e7921003 ldr r1, [r2, r3] --------------從GOT entry找到yyy的絕對地址
578: e59f3024 ldr r3, [pc, #36] ; 5a4 <.text+0x110> ---r3被賦值0x5678
57c: e5813000 str r3, [r1] ---------------給yyy賦值
……
59c: 00008138 .word 0x00008138 -----------指令到GOT的偏移
5a0: 00000010 .word 0x00000010 -----------yyy符號在GOT中的offset
5a4: 00005678 .word 0x00005678
5a8: 00000018 .word 0x00000018
雖然不知道GOT的絕對地址,但是在靜態連結的時候,程式碼段的程式碼和GOT的偏移是已經確定的(loading的時候是按照program header中的資訊進行loading,code segment和data segment是連續的),因此,在指令中可以通過59c這個橋樑獲取GOT的首地址,加上entry偏移就可以獲取指定符號的GOT入口地址,從該GOT入口地址中可以取出runtime的符號的絕對地址。
相關文章
- 學習電腦編碼utf-8,ansi編碼的基礎知識等
- 具體數學:電腦科學基礎(第2版)
- 具體數學——電腦科學基礎 審讀有感
- 《Web 自動化》基礎知識腦圖Web
- 你對電腦還一無所知?3分鐘帶你全面瞭解電腦基礎知識
- 【資料結構】棧的基礎知識(無程式碼)資料結構
- 【資料結構】串的基礎知識(無程式碼)資料結構
- Camera基礎知識四
- JavaSE基礎知識分享(四)Java
- MySQL 基礎知識梳理學習(四)—-GTIDMySql
- 程式基礎知識
- 【資料結構】佇列的基礎知識(無程式碼)資料結構佇列
- Linux 動態庫相關知識整理Linux
- OpenSSL 入門:密碼學基礎知識密碼學
- Linux 動態庫相關知識總結Linux
- JavaSE基礎知識學習-----Static關鍵字Java
- Oracle相關基礎知識Oracle
- 鎖相關基礎知識
- mysql資料庫學習基礎知識整理MySql資料庫
- Java基礎知識整理之程式碼塊Java
- 協程庫基礎知識
- 資料庫基礎知識資料庫
- 美國四種薪酬最高的電腦科學工作
- ZooKeeper 基礎知識、部署和應用程式
- 電腦鍵盤功能基礎知識 電腦鍵碟符號快捷鍵大全介紹符號
- 圖形學基礎知識
- 【ASM學習】基礎知識ASM
- BSN-DDC 基礎網路關鍵知識點(四)DDC的生成和管理
- JavaSE基礎知識學習—–抽象類和介面Java抽象
- JavaSE基礎知識學習-----抽象類和介面Java抽象
- 碼農 駭客 程式設計師 開發者和電腦科學家的區別程式設計師
- 【編測編學】MySQL資料庫基礎知識MySql資料庫
- 資料庫學習筆記 - MySQL基礎知識資料庫筆記MySql
- 【RAC】RAC相關基礎知識
- 程式設計基礎知識程式設計
- SSL和CA基礎知識
- GMAC和PHY基礎知識Mac
- 電腦硬體相關簡單知識