GNU binutils 裡的九種武器
二進位制分析是計算機行業中最被低估的技能。
想象一下,在無法訪問軟體的原始碼時,但仍然能夠理解軟體的實現方式,在其中找到漏洞,並且更厲害的是還能修復錯誤。所有這些都是在只有二進位制檔案時做到的。這聽起來就像是超能力,對吧?
你也可以擁有這樣的超能力,GNU 二進位制實用程式(binutils)就是一個很好的起點。GNU binutils 是一個二進位制工具集,預設情況下所有 Linux 發行版中都會安裝這些二進位制工具。
二進位制分析是計算機行業中最被低估的技能。它主要由惡意軟體分析師、反向工程師和使用底層軟體的人使用。
本文探討了 binutils 可用的一些工具。我使用的是 RHEL,但是這些示例應該在任何 Linux 發行版上可以執行。
[~]# cat /etc/redhat-release
Red Hat Enterprise Linux Server release 7.6 (Maipo)
[~]#
[~]# uname -r
3.10.0-957.el7.x86_64
[~]#
請注意,某些打包命令(例如 rpm
)在基於 Debian 的發行版中可能不可用,因此請使用等效的 dpkg
命令替代。
軟體開發的基礎知識
在開源世界中,我們很多人都專注於原始碼形式的軟體。當軟體的原始碼隨時可用時,很容易獲得原始碼的副本,開啟喜歡的編輯器,喝杯咖啡,然後就可以開始探索了。
但是原始碼不是在 CPU 上執行的程式碼,在 CPU 上執行的是二進位制或者說是機器語言指令。二進位制或可執行檔案是編譯原始碼時獲得的。熟練的除錯人員深諳通常這種差異。
編譯的基礎知識
在深入研究 binutils 軟體包本身之前,最好先了解編譯的基礎知識。
編譯是將程式從某種程式語言(如 C/C++)的原始碼(文字形式)轉換為機器程式碼的過程。
機器程式碼是 CPU(或一般而言,硬體)可以理解的 1 和 0 的序列,因此可以由 CPU 執行或執行。該機器碼以特定格式儲存到檔案,通常稱為可執行檔案或二進位制檔案。在 Linux(和使用 Linux 相容二進位制的 BSD)上,這稱為 ELF(可執行和可連結格式)。
在生成給定的原始檔的可執行檔案或二進位制檔案之前,編譯過程將經歷一系列複雜的步驟。以這個源程式(C 程式碼)為例。開啟你喜歡的編輯器,然後鍵入以下程式:
#include <stdio.h>
int main(void)
{
printf("Hello World\n");
return 0;
}
步驟 1:用 cpp 預處理
C 預處理程式(cpp)用於擴充套件所有宏並將標頭檔案包含進來。在此示例中,標頭檔案 stdio.h
將被包含在原始碼中。stdio.h
是一個標頭檔案,其中包含有關程式內使用的 printf
函式的資訊。對原始碼執行 cpp
,其結果指令儲存在名為 hello.i
的檔案中。可以使用文字編輯器開啟該檔案以檢視其內容。列印 “hello world” 的原始碼在該檔案的底部。
[testdir]# cat hello.c
#include <stdio.h>
int main(void)
{
printf("Hello World\n");
return 0;
}
[testdir]#
[testdir]# cpp hello.c > hello.i
[testdir]#
[testdir]# ls -lrt
total 24
-rw-r--r--. 1 root root 76 Sep 13 03:20 hello.c
-rw-r--r--. 1 root root 16877 Sep 13 03:22 hello.i
[testdir]#
步驟 2:用 gcc 編譯
在此階段,無需建立目標檔案就將步驟 1 中生成的預處理原始碼轉換為組合語言指令。這個階段使用 GNU 編譯器集合(gcc)。對 hello.i
檔案執行帶有 -S
選項的 gcc
命令後,它將建立一個名為 hello.s
的新檔案。該檔案包含該 C 程式的組合語言指令。
你可以使用任何編輯器或 cat
命令檢視其內容。
[testdir]#
[testdir]# gcc -Wall -S hello.i
[testdir]#
[testdir]# ls -l
total 28
-rw-r--r--. 1 root root 76 Sep 13 03:20 hello.c
-rw-r--r--. 1 root root 16877 Sep 13 03:22 hello.i
-rw-r--r--. 1 root root 448 Sep 13 03:25 hello.s
[testdir]#
[testdir]# cat hello.s
.file "hello.c"
.section .rodata
.LC0:
.string "Hello World"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $.LC0, %edi
call puts
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-36)"
.section .note.GNU-stack,"",@progbits
[testdir]#
步驟 3:用 as 彙編
彙編器的目的是將組合語言指令轉換為機器語言程式碼,並生成副檔名為 .o
的目標檔案。此階段使用預設情況下在所有 Linux 平臺上都可用的 GNU 彙編器。
testdir]# as hello.s -o hello.o
[testdir]#
[testdir]# ls -l
total 32
-rw-r--r--. 1 root root 76 Sep 13 03:20 hello.c
-rw-r--r--. 1 root root 16877 Sep 13 03:22 hello.i
-rw-r--r--. 1 root root 1496 Sep 13 03:39 hello.o
-rw-r--r--. 1 root root 448 Sep 13 03:25 hello.s
[testdir]#
現在,你有了第一個 ELF 格式的檔案;但是,還不能執行它。稍後,你將看到“目標檔案”和“可執行檔案”之間的區別。
[testdir]# file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
步驟 4:用 ld 連結
這是編譯的最後階段,將目標檔案連結以建立可執行檔案。可執行檔案通常需要外部函式,這些外部函式通常來自系統庫(libc
)。
你可以使用 ld
命令直接呼叫連結器;但是,此命令有些複雜。相反,你可以使用帶有 -v
(詳細)標誌的 gcc
編譯器,以瞭解連結是如何發生的。(使用 ld
命令進行連結作為一個練習,你可以自行探索。)
[testdir]# gcc -v hello.o
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/lto-wrapper
Target: x86_64-redhat-linux
Configured with: ../configure --prefix=/usr --mandir=/usr/share/man [...] --build=x86_64-redhat-linux
Thread model: posix
gcc version 4.8.5 20150623 (Red Hat 4.8.5-36) (GCC)
COMPILER_PATH=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/:/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/:[...]:/usr/lib/gcc/x86_64-redhat-linux/
LIBRARY_PATH=/usr/lib/gcc/x86_64-redhat-linux/4.8.5/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/:/lib/../lib64/:/usr/lib/../lib64/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-v' '-mtune=generic' '-march=x86-64'
/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/collect2 --build-id --no-add-needed --eh-frame-hdr --hash-style=gnu [...]/../../../../lib64/crtn.o
[testdir]#
執行此命令後,你應該看到一個名為 a.out
的可執行檔案:
[testdir]# ls -l
total 44
-rwxr-xr-x. 1 root root 8440 Sep 13 03:45 a.out
-rw-r--r--. 1 root root 76 Sep 13 03:20 hello.c
-rw-r--r--. 1 root root 16877 Sep 13 03:22 hello.i
-rw-r--r--. 1 root root 1496 Sep 13 03:39 hello.o
-rw-r--r--. 1 root root 448 Sep 13 03:25 hello.s
對 a.out
執行 file
命令,結果表明它確實是 ELF 可執行檔案:
[testdir]# file a.out
a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=48e4c11901d54d4bf1b6e3826baf18215e4255e5, not stripped
執行該可執行檔案,看看它是否如原始碼所示工作:
[testdir]# ./a.out Hello World
工作了!在幕後發生了很多事情它才在螢幕上列印了 “Hello World”。想象一下在更復雜的程式中會發生什麼。
探索 binutils 工具
上面這個練習為使用 binutils 軟體包中的工具提供了良好的背景。我的系統帶有 binutils 版本 2.27-34;你的 Linux 發行版上的版本可能有所不同。
[~]# rpm -qa | grep binutils
binutils-2.27-34.base.el7.x86_64
binutils 軟體包中提供了以下工具:
[~]# rpm -ql binutils-2.27-34.base.el7.x86_64 | grep bin/
/usr/bin/addr2line
/usr/bin/ar
/usr/bin/as
/usr/bin/c++filt
/usr/bin/dwp
/usr/bin/elfedit
/usr/bin/gprof
/usr/bin/ld
/usr/bin/ld.bfd
/usr/bin/ld.gold
/usr/bin/nm
/usr/bin/objcopy
/usr/bin/objdump
/usr/bin/ranlib
/usr/bin/readelf
/usr/bin/size
/usr/bin/strings
/usr/bin/strip
上面的編譯練習已經探索了其中的兩個工具:用作彙編器的 as
命令,用作連結器的 ld
命令。繼續閱讀以瞭解上述 GNU binutils 軟體包工具中的其他七個。
readelf:顯示 ELF 檔案資訊
上面的練習提到了術語“目標檔案”和“可執行檔案”。使用該練習中的檔案,透過帶有 -h
(標題)選項的 readelf
命令,以將檔案的 ELF 標題轉儲到螢幕上。請注意,以 .o
副檔名結尾的目標檔案顯示為 Type: REL (Relocatable file)
(可重定位檔案):
[testdir]# readelf -h hello.o
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 [...]
[...]
Type: REL (Relocatable file)
[...]
如果嘗試執行此目標檔案,會收到一條錯誤訊息,指出無法執行。這僅表示它尚不具備在 CPU 上執行所需的資訊。
請記住,你首先需要使用 chmod
命令在物件檔案上新增 x
(可執行位),否則你將得到“許可權被拒絕”的錯誤。
[testdir]# ./hello.o
bash: ./hello.o: Permission denied
[testdir]# chmod +x ./hello.o
[testdir]#
[testdir]# ./hello.o
bash: ./hello.o: cannot execute binary file
如果對 a.out
檔案嘗試相同的命令,則會看到其型別為 EXEC (Executable file)
(可執行檔案)。
[testdir]# readelf -h a.out
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
[...] Type: EXEC (Executable file)
如上所示,該檔案可以直接由 CPU 執行:
[testdir]# ./a.out Hello World
readelf
命令可提供有關二進位制檔案的大量資訊。在這裡,它會告訴你它是 ELF 64 位格式,這意味著它只能在 64 位 CPU 上執行,而不能在 32 位 CPU 上執行。它還告訴你它應在 X86-64(Intel/AMD)架構上執行。該二進位制檔案的入口點是地址 0x400430
,它就是 C 源程式中 main
函式的地址。
在你知道的其他系統二進位制檔案上嘗試一下 readelf
命令,例如 ls
。請注意,在 RHEL 8 或 Fedora 30 及更高版本的系統上,由於安全原因改用了位置無關可執行檔案(PIE),因此你的輸出(尤其是 Type:
)可能會有所不同。
[testdir]# readelf -h /bin/ls
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
使用 ldd
命令瞭解 ls
命令所依賴的系統庫,如下所示:
[testdir]# ldd /bin/ls
linux-vdso.so.1 => (0x00007ffd7d746000)
libselinux.so.1 => /lib64/libselinux.so.1 (0x00007f060daca000)
libcap.so.2 => /lib64/libcap.so.2 (0x00007f060d8c5000)
libacl.so.1 => /lib64/libacl.so.1 (0x00007f060d6bc000)
libc.so.6 => /lib64/libc.so.6 (0x00007f060d2ef000)
libpcre.so.1 => /lib64/libpcre.so.1 (0x00007f060d08d000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007f060ce89000)
/lib64/ld-linux-x86-64.so.2 (0x00007f060dcf1000)
libattr.so.1 => /lib64/libattr.so.1 (0x00007f060cc84000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f060ca68000)
對 libc
庫檔案執行 readelf
以檢視它是哪種檔案。正如它指出的那樣,它是一個 DYN (Shared object file)
(共享物件檔案),這意味著它不能直接執行;必須由內部使用了該庫提供的任何函式的可執行檔案使用它。
[testdir]# readelf -h /lib64/libc.so.6
ELF Header:
Magic: 7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - GNU
ABI Version: 0
Type: DYN (Shared object file)
size:列出節的大小和全部大小
size
命令僅適用於目標檔案和可執行檔案,因此,如果嘗試在簡單的 ASCII 檔案上執行它,則會丟擲錯誤,提示“檔案格式無法識別”。
[testdir]# echo "test" > file1
[testdir]# cat file1
test
[testdir]# file file1
file1: ASCII text
[testdir]# size file1
size: file1: File format not recognized
現在,在上面的練習中,對目標檔案和可執行檔案執行 size
命令。請注意,根據 size
命令的輸出可以看出,可執行檔案(a.out
)的資訊要比目標檔案(hello.o
)多得多:
[testdir]# size hello.o
text data bss dec hex filename
89 0 0 89 59 hello.o
[testdir]# size a.out
text data bss dec hex filename
1194 540 4 1738 6ca a.out
但是這裡的 text
、data
和 bss
節是什麼意思?
text
節是指二進位制檔案的程式碼部分,其中包含所有可執行指令。data
節是所有初始化資料所在的位置,bss
節是所有未初始化資料的儲存位置。(LCTT 譯註:一般來說,在靜態的映像檔案中,各個部分稱之為節,而在執行時的各個部分稱之為段,有時統稱為段。)
比較其他一些可用的系統二進位制檔案的 size
結果。
對於 ls
命令:
[testdir]# size /bin/ls
text data bss dec hex filename
103119 4768 3360 111247 1b28f /bin/ls
只需檢視 size
命令的輸出,你就可以看到 gcc
和 gdb
是比 ls
大得多的程式:
[testdir]# size /bin/gcc
text data bss dec hex filename
755549 8464 81856 845869 ce82d /bin/gcc
[testdir]# size /bin/gdb
text data bss dec hex filename
6650433 90842 152280 6893555 692ff3 /bin/gdb
strings:列印檔案中的可列印字串
在 strings
命令中新增 -d
標誌以僅顯示 data
節中的可列印字元通常很有用。
hello.o
是一個目標檔案,其中包含列印出 Hello World
文字的指令。因此,strings
命令的唯一輸出是 Hello World
。
[testdir]# strings -d hello.o
Hello World
另一方面,在 a.out
(可執行檔案)上執行 strings
會顯示在連結階段該二進位制檔案中包含的其他資訊:
[testdir]# strings -d a.out
/lib64/ld-linux-x86-64.so.2
!^BU
libc.so.6
puts
__libc_start_main
__gmon_start__
GLIBC_2.2.5
UH-0
UH-0
=(
[]A\A]A^A_
Hello World
;*3$"
objdump:顯示目標檔案資訊
另一個可以從二進位制檔案中轉儲機器語言指令的 binutils 工具稱為 objdump
。使用 -d
選項,可從二進位制檔案中反彙編出所有彙編指令。
回想一下,編譯是將原始碼指令轉換為機器程式碼的過程。機器程式碼僅由 1 和 0 組成,人類難以閱讀。因此,它有助於將機器程式碼表示為組合語言指令。組合語言是什麼樣的?請記住,組合語言是特定於體系結構的;由於我使用的是 Intel(x86-64)架構,因此如果你使用 ARM 架構編譯相同的程式,指令將有所不同。
[testdir]# objdump -d hello.o
hello.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000
:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: bf 00 00 00 00 mov $0x0,%edi
9: e8 00 00 00 00 callq e
e: b8 00 00 00 00 mov $0x0,%eax
13: 5d pop %rbp
14: c3 retq
該輸出乍一看似乎令人生畏,但請花一點時間來理解它,然後再繼續。回想一下,.text
節包含所有的機器程式碼指令。彙編指令可以在第四列中看到(即 push
、mov
、callq
、pop
、retq
等)。這些指令作用於暫存器,暫存器是 CPU 內建的儲存器位置。本示例中的暫存器是 rbp
、rsp
、edi
、eax
等,並且每個暫存器都有特殊的含義。
現在對可執行檔案(a.out
)執行 objdump
並檢視得到的內容。可執行檔案的 objdump
的輸出可能很大,因此我使用 grep
命令將其縮小到 main
函式:
[testdir]# objdump -d a.out | grep -A 9 main\>
000000000040051d
:
40051d: 55 push %rbp
40051e: 48 89 e5 mov %rsp,%rbp
400521: bf d0 05 40 00 mov $0x4005d0,%edi
400526: e8 d5 fe ff ff callq 400400
40052b: b8 00 00 00 00 mov $0x0,%eax
400530: 5d pop %rbp
400531: c3 retq
請注意,這些指令與目標檔案 hello.o
相似,但是其中包含一些其他資訊:
- 目標檔案
hello.o
具有以下指令:callq e
- 可執行檔案
a.out
由以下指令組成,該指令帶有一個地址和函式:callq 400400 <puts@plt>
上面的彙編指令正在呼叫puts
函式。請記住,你在原始碼中使用了一個printf
函式。編譯器插入了對puts
庫函式的呼叫,以將Hello World
輸出到螢幕。
檢視 put
上方一行的說明:
- 目標檔案
hello.o
有個指令mov
:mov $0x0,%edi
- 可執行檔案
a.out
的mov
指令帶有實際地址($0x4005d0
)而不是$0x0
:mov $0x4005d0,%edi
該指令將二進位制檔案中地址 $0x4005d0
處存在的內容移動到名為 edi
的暫存器中。
這個儲存位置的內容中還能是別的什麼嗎?是的,你猜對了:它就是文字 Hello, World
。你是如何確定的?
readelf
命令使你可以將二進位制檔案(a.out
)的任何節轉儲到螢幕上。以下要求它將 .rodata
(這是隻讀資料)轉儲到螢幕上:
[testdir]# readelf -x .rodata a.out
Hex dump of section '.rodata':
0x004005c0 01000200 00000000 00000000 00000000 ....
0x004005d0 48656c6c 6f20576f 726c6400 Hello World.
你可以在右側看到文字 Hello World
,在左側可以看到其二進位制格式的地址。它是否與你在上面的 mov
指令中看到的地址匹配?是的,確實匹配。
strip:從目標檔案中剝離符號
該命令通常用於在將二進位制檔案交付給客戶之前減小二進位制檔案的大小。
請記住,由於重要資訊已從二進位制檔案中刪除,因此它會妨礙除錯。但是,這個二進位制檔案可以完美地執行。
對 a.out
可執行檔案執行該命令,並注意會發生什麼。首先,透過執行以下命令確保二進位制檔案沒有被剝離(not stripped
):
[testdir]# file a.out
a.out: ELF 64-bit LSB executable, x86-64, [......] not stripped
另外,在執行 strip
命令之前,請記下二進位制檔案中最初的位元組數:
[testdir]# du -b a.out
8440 a.out
現在對該可執行檔案執行 strip
命令,並使用 file
命令以確保正常完成:
[testdir]# strip a.out
[testdir]# file a.out a.out: ELF 64-bit LSB executable, x86-64, [......] stripped
剝離該二進位制檔案後,此小程式的大小從之前的 8440
位元組減小為 6296
位元組。對於這樣小的一個程式都能有這麼大的空間節省,難怪大型程式經常被剝離。
[testdir]# du -b a.out
6296 a.out
addr2line:轉換地址到檔名和行號
addr2line
工具只是在二進位制檔案中查詢地址,並將其與 C 原始碼程式中的行進行匹配。很酷,不是嗎?
為此編寫另一個測試程式;只是這一次確保使用 gcc
的 -g
標誌進行編譯,這將為二進位制檔案新增其它除錯資訊,幷包含有助於除錯的行號(由原始碼中提供):
[testdir]# cat -n atest.c
1 #include <stdio.h>
2
3 int globalvar = 100;
4
5 int function1(void)
6 {
7 printf("Within function1\n");
8 return 0;
9 }
10
11 int function2(void)
12 {
13 printf("Within function2\n");
14 return 0;
15 }
16
17 int main(void)
18 {
19 function1();
20 function2();
21 printf("Within main\n");
22 return 0;
23 }
用 -g
標誌編譯並執行它。正如預期:
[testdir]# gcc -g atest.c
[testdir]# ./a.out
Within function1
Within function2
Within main
現在使用 objdump
來標識函式開始的記憶體地址。你可以使用 grep
命令來過濾出所需的特定行。函式的地址在下面突出顯示(55 push %rbp
前的地址):
[testdir]# objdump -d a.out | grep -A 2 -E 'main>:|function1>:|function2>:'
000000000040051d :
40051d: 55 push %rbp
40051e: 48 89 e5 mov %rsp,%rbp
--
0000000000400532 :
400532: 55 push %rbp
400533: 48 89 e5 mov %rsp,%rbp
--
0000000000400547
:
400547: 55 push %rbp
400548: 48 89 e5 mov %rsp,%rbp
現在,使用 addr2line
工具從二進位制檔案中的這些地址對映到 C 原始碼匹配的地址:
[testdir]# addr2line -e a.out 40051d
/tmp/testdir/atest.c:6
[testdir]#
[testdir]# addr2line -e a.out 400532
/tmp/testdir/atest.c:12
[testdir]#
[testdir]# addr2line -e a.out 400547
/tmp/testdir/atest.c:18
它說 40051d
從原始檔 atest.c
中的第 6
行開始,這是 function1
的起始大括號({
)開始的行。function2
和 main
的輸出也匹配。
nm:列出目標檔案的符號
使用上面的 C 程式測試 nm
工具。使用 gcc
快速編譯並執行它。
[testdir]# gcc atest.c
[testdir]# ./a.out
Within function1
Within function2
Within main
現在執行 nm
和 grep
獲取有關函式和變數的資訊:
[testdir]# nm a.out | grep -Ei 'function|main|globalvar'
000000000040051d T function1
0000000000400532 T function2
000000000060102c D globalvar
U __libc_start_main@@GLIBC_2.2.5
0000000000400547 T main
你可以看到函式被標記為 T
,它表示 text
節中的符號,而變數標記為 D
,表示初始化的 data
節中的符號。
想象一下在沒有原始碼的二進位制檔案上執行此命令有多大用處?這使你可以窺視內部並瞭解使用了哪些函式和變數。當然,除非二進位制檔案已被剝離,這種情況下它們將不包含任何符號,因此 nm
就命令不會很有用,如你在此處看到的:
[testdir]# strip a.out
[testdir]# nm a.out | grep -Ei 'function|main|globalvar'
nm: a.out: no symbols
結論
GNU binutils 工具為有興趣分析二進位制檔案的人提供了許多選項,這只是它們可以為你做的事情的冰山一角。請閱讀每種工具的手冊頁,以瞭解有關它們以及如何使用它們的更多資訊。
via: https://opensource.com/article/19/10/gnu-binutils
作者:Gaurav Kamathe 選題:lujun9972 譯者:wxy 校對:wxy
本文由 LCTT 原創編譯,Linux中國 榮譽推出
相關文章
- 讓 Python 程式碼更易維護的七種武器Python
- Linux 命令列引數的三種風格 Unix/Posix、BSD、GNULinux命令列
- 成為 Linux 運維高手必備的七種“武器”!Linux運維
- 是時候更新手裡的武器了—Jetpack最全簡析Jetpack
- 跨域的九種解決方法跨域
- Tungsten Fabric入門寶典丨TF元件的七種“武器”元件
- GNU與Linux 的關係Linux
- 使用 GNU Screen 的小技巧
- gnu inline asminlineASM
- 九種常見的資料分析模型模型
- 解鎖跨域的九種姿勢跨域
- GNU Guix 將終止對 Linux 核心的支援,全面替換為 GNU HurdGUILinux
- MinGW -- Minimalist GNU for WindowsWindows
- 我們是如何將史上最優雅的武器做進遊戲裡的?遊戲
- 圖解九種常見的設計模式圖解設計模式
- 九種排序演算法的 JavaScript 實現排序演算法JavaScript
- 九種方式實現跨域跨域
- vscode原始碼分析【九】視窗裡的主要元素VSCode原始碼
- 九 江 哪 裡 有 開 發 票
- Selenium裡的三種等待方式
- Java中常見字串拼接九種方式Java字串
- 九種跨域方式實現原理跨域
- [SDR] GNU Radio 系列教程(十四) —— GNU Radio 低階到高階用法的分水嶺 ZMQ 的使用詳解MQ
- 武器庫
- 『分析和解決問題的7種武器』今日資料行業日報(2019.08.16)行業
- 安卓動態除錯七種武器之孔雀翎 – Ida Pro安卓除錯
- 在D社發行的這款獨立遊戲裡,魚叉是你的主要武器遊戲
- 圖解 SQL 裡的各種 JOIN圖解SQL
- Unix、Linux、GNU 關係梳理Linux
- Java 中九種 Map 的遍歷方式,你一般用的是哪種呢?Java
- C99 和 GNU99 的區別
- 修改Debian GNU/Linux的預設等寬字型Linux
- 安卓動態除錯七種武器之離別鉤 – Hooking(上)安卓除錯Hook
- 安卓動態除錯七種武器之長生劍 - Smali Instrumentation安卓除錯
- 安卓動態除錯七種武器之離別鉤 – Hooking(下)安卓除錯Hook
- 值得了解的九種樹形資料結構 - Franco資料結構
- SAP Fiori裡的兩種鎖機制
- 如何在 Linux 上使用 GNU sedLinux