
緣起
云原生復(fù)雜性
在 200x 年時代,服務(wù)端軟件架構(gòu),組成的復(fù)雜度,異構(gòu)程度相對于云原生,可謂簡單很多。那個年代,大多數(shù)基礎(chǔ)組件,要么由使用企業(yè)開發(fā),要么是購買組件服務(wù)支持。
到了 201x 年代,開源運動,去 IOE 運動興起。企業(yè)更傾向選擇開源基礎(chǔ)組件。然而開源基礎(chǔ)的維護和問題解決成本其實并不是看起來那么低。給你源碼,你以為就什么都看得透嗎?對于企業(yè),現(xiàn)在起碼有幾個大問題:
從高處看:
- 企業(yè)要投入多少人力才、財力可以找到或培養(yǎng)一個看得透開源基礎(chǔ)組件的人?
- 開源的版本、安全漏洞、更迭快速,即使專業(yè)人才也很難快速看得透運行期的軟件行為。
- 組件之間錯綜復(fù)雜的依賴、調(diào)用關(guān)系,再加上版本依賴和更迭,沒有可能運行過完全相同環(huán)境的測試(哪怕你用了vm/docker image)
- 或者你還很迷戀
向后兼容,即使它已經(jīng)傷害過無數(shù)程序員的心和夜晚 - 就像 古希臘哲學(xué)家赫拉克利特說:no one can step into the same river once(人不能兩次踏進同一條河流)
- 或者你還很迷戀
從細節(jié)看:
對于大型的開源項目,一般企業(yè)沒可能投入人力看懂全部代碼(注意,是看懂,不是看過)。而企業(yè)真正關(guān)心或使用的,可能只是一小部分和切身故障相關(guān)的子模塊。
-
對于大型的開源項目,即使你認為看懂全部代碼。你也不太可能了解全部運行期的狀態(tài)。哪怕是項目作者,也不一定可以。
- 項目的作者不在企業(yè),也不可能完全了解企業(yè)中數(shù)據(jù)的特性。更何況無處不在的 bug
-
開源軟件的精神在于開放與 free(這里不是指免費,這里只能用英文),而 free 不單單是 read only,它還是 writable 的。
- 開源軟件大都不是大公司中某天才產(chǎn)品經(jīng)理、天才構(gòu)架師設(shè)計出來。而是眾多使用者一起打磨出來的。但如果要看懂全部代碼才能 writable,恐怕沒人可以修改 Linux 內(nèi)核了。
-
靜態(tài)的代碼。這點我認為是最重要的。我們所謂的看懂全部代碼,是指靜態(tài)的代碼。但有經(jīng)驗的程序員都知道,代碼只有跑起來,才真正讓人看得通透。而能分析一個跑起來的程序,才可以說,我看懂全部代碼。
- 這讓我想起,一般的 code review,都在 review 什么?
云原生現(xiàn)場分析的難
賣了半天的關(guān)子,那么有什么方法可以賣弄?可以快速理點,分析開源項目運行期行為?
- 加日志。
- 如果要解決的問題剛才源碼中有日志,或者提供日志開關(guān),當(dāng)然就打開完事。收工開飯。但這運氣得多好?
- 修改開源源碼,加入日志,來個緊急上線。這樣你得和運維關(guān)系有多鐵?你確定加一次就夠了嗎?
- 語言級別的動態(tài) instrumentation 注入代碼
- 在注入代碼中分析數(shù)據(jù)或出日志。如 alibaba/arthas 。golang instrumentation
- 這對語言有要求,如果是 c/c++ 等就 愛莫能助 了。
- 對性能影響一般也不少。
- debug
- java debug / golang Delve / gdb 等,都有一定的使用門檻,如程序打包時需要包含了 debug 信息。這在當(dāng)下喜歡計較 image 大小的年代,debug 信息多被翦掉。同時,斷點時可能掛起線程甚至整個進程。生產(chǎn)環(huán)境上發(fā)生就是災(zāi)難。
- uprobe/kprobe/eBPF
- 在上面方法都不可行時,這個方法值得一試。下面,我們分析一下,什么是 uprobe/kprobe/eBPF。為何有價值。
逆向工程思維
我們知道現(xiàn)在大部分程序都是用高級語言編碼,再編譯生成可執(zhí)行的文件( .exe / ELF ) 或中間文件在運行期 JIT 編譯。最終一定要生成計算機指令,計算機才能運行。對于開源項目,如果我們找到了這堆生成的計算機指令和源代碼之間映射關(guān)系。然后:
- 在這堆計算機指令的一個合理的位置(可以先假設(shè)這個位置就是我們關(guān)注的一個高級語言函數(shù)的入口)中放入一個
鉤子 - 如果程序運行到
鉤子時,我們可以探視:- 當(dāng)前程序的函數(shù)調(diào)用堆棧
- 當(dāng)前函數(shù)調(diào)用的參數(shù)、返回值
- 當(dāng)前進程的靜態(tài)/全局變量
對于開源項目,知道運行期的實際狀態(tài)是現(xiàn)場分析問題解決的關(guān)鍵。
由于不想讓本文開頭過于理論,嚇跑人,我把 細說逆向工程思維 一節(jié)移到最后。
實踐
我之前寫技術(shù)文章很少寫幾千字還沒一行代碼。不過最近不知道是年紀漸長,還是怎的,總想多說點廢話。
Show me the code.
實踐目標
我們探視所謂的云原生服務(wù)網(wǎng)格之背骨的 Envoy sidecar 代理為例子,看看 Envoy 啟動過程和建立客戶端連接過程中:
- 是在什么代碼去監(jiān)聽 TCP 端口
- 監(jiān)聽的 socket 是否設(shè)置了中外馳名的 SO_REUSEADDR
- TCP 連接又是否啟用了臭名昭著的增大網(wǎng)絡(luò)時延的 Nagle 算法(還是相反 socket 設(shè)置了 TCP_NODELAY),見 https://en.wikipedia.org/wiki/Nagle%27s_algorithm
說了那么多廢話,主角來了,eBPF技術(shù)和我們這次要用的工具 bpftrace。
先說說我的環(huán)境:
- Ubuntu Linux 20.04
- 系統(tǒng)默認的 bpftrace v0.9.4 (這版本有問題,后面說)
Hello World
上面的 3 實踐目標很“偉大”。但我們在實現(xiàn)前,還是先來個小目標,寫個 Hello World 吧。
我們知道 envoy 源碼的主入口在 main_common.cc 的:
int MainCommon::main(int argc, char** argv, PostServerHook hook) {
...
}
我們目標是在 envoy 初始化時,調(diào)用這個函數(shù)時輸出一行信息,代表成功攔截。
首先看看 envoy 可執(zhí)行文件中帶有的函數(shù)地址元信息:
? ~ readelf -s --wide ./envoy | egrep 'MainCommon.*main'
114457: 00000000016313c0 635 FUNC GLOBAL DEFAULT 14 _ZN5Envoy10MainCommon4mainEiPPcNSt3__18functionIFvRNS_6Server8InstanceEEEE
這里需要說明一下,c++ 代碼編譯時,內(nèi)部表示函數(shù)的名字不是直接使用源碼的名字,是規(guī)范化變形(mangling)后的名字(可以用 c++filt 命令手工轉(zhuǎn)換)。這里我們得知變形后的函數(shù)名是:_ZN5Envoy10MainCommon4mainEiPPcNSt3__18functionIFvRNS_6Server8InstanceEEEE。于是可以用 bpftrace去攔截了。
bpftrace -e 'uprobe:./envoy:_ZN5Envoy10MainCommon4mainEiPPcNSt3__18functionIFvRNS_6Server8InstanceEEEE { printf("Hello world: Got MainCommon::main"); }'
這時,在另外一個終端中運行 envoy
./envoy -c envoy-demo.yaml
卡脖子的現(xiàn)實
在我初學(xué)攝影時,老師告訴我一個情況叫:Beginner's luck。而技術(shù)界往往相反。這次,我什么都沒攔截到。用自以為是的經(jīng)驗摸索了各種方法,均無果。我在這種摸索、無果的循環(huán)中折騰了大概半年……
突破
折騰了大概半年后,我實在想放棄了。想不到,一個 Hello World 小目標也完成不了。直到一天,我醒悟到說到底是自己基礎(chǔ)知識不好,才不能定位到問題的根源。于是惡補了 程序鏈接、ELF文件格式、ELF 加載進程內(nèi)存 等知識。后來,千辛萬苦最于找到根本原因(如果一定要一句話說完,就是 bpftrace 舊版本錯誤解釋了函數(shù)元信息的地址 )。相關(guān)的細節(jié)我將寫成一編獨立的技術(shù)文章。這里先不多說。解決方法卻很簡單,升級 bpftrace,我直接自己編譯了 bpftrace v0.14.1 。
終于,在啟動 envoy 后輸出了:
Hello world: Got MainCommon::main
^C
實踐
我嘗試不按正常的順序思維講這部分。因為一開始去分析實現(xiàn)原理,腳本程序,還不如先瀏覽一下代碼,然后運行一次給大家看。
我們先簡單瀏覽 bpftrace 程序,trace-envoy-socket.bt :
#!/usr/local/bin/bpftrace
#include <linux/in.h>
#include <linux/in6.h>
BEGIN
{
@fam2str[AF_UNSPEC] = "AF_UNSPEC";
@fam2str[AF_UNIX] = "AF_UNIX";
@fam2str[AF_INET] = "AF_INET";
@fam2str[AF_INET6] = "AF_INET6";
}
tracepoint:syscalls:sys_enter_setsockopt
/pid == $1/
{
// socket opts: https://elixir.bootlin.com/linux/v5.16.3/source/include/uapi/linux/tcp.h#L92
$fd = args->fd;
$optname = args->optname;
$optval = args->optval;
$optval_int = *$optval;
$optlen = args->optlen;
printf("\n########## setsockopt() ##########\n");
printf("comm:%-16s: setsockopt: fd=%d, optname=%d, optval=%d, optlen=%d. stack: %s\n", comm, $fd, $optname, $optval_int, $optlen, ustack);
}
tracepoint:syscalls:sys_enter_bind
/pid == $1/
{
// printf("bind");
$sa = (struct sockaddr *)args->umyaddr;
$fd = args->fd;
printf("\n########## bind() ##########\n");
if ($sa->sa_family == AF_INET || $sa->sa_family == AF_INET6) {
// printf("comm:%-16s: bind AF_INET(6): %-6d %-16s %-3d \n", comm, pid, comm, $sa->sa_family);
if ($sa->sa_family == AF_INET) { //IPv4
$s = (struct sockaddr_in *)$sa;
$port = ($s->sin_port >> 8) |
(($s->sin_port << 8) & 0xff00);
$bind_ip = ntop(AF_INET, $s->sin_addr.s_addr);
printf("comm:%-16s: bind AF_INET: ip:%-16s port:%-5d fd=%d \n", comm,
$bind_ip,
$port, $fd);
} else { //IPv6
$s6 = (struct sockaddr_in6 *)$sa;
$port = ($s6->sin6_port >> 8) |
(($s6->sin6_port << 8) & 0xff00);
$bind_ip = ntop(AF_INET6, $s6->sin6_addr.in6_u.u6_addr8);
printf("comm:%-16s: bind AF_INET6:%-16s %-5d \n", comm,
$bind_ip,
$port);
}
printf("stack: %s\n", ustack);
// @bind[comm, args->uservaddr->sa_family,
// @fam2str[args->uservaddr->sa_family]] = count();
}
}
//tracepoint:syscalls:sys_enter_accept,
tracepoint:syscalls:sys_enter_accept4
/pid == $1/
{
@sockaddr[tid] = args->upeer_sockaddr;
}
//tracepoint:syscalls:sys_exit_accept,
tracepoint:syscalls:sys_exit_accept4
/pid == $1/
{
if( @sockaddr[tid] != 0 ) {
$sa = (struct sockaddr *)@sockaddr[tid];
if ($sa->sa_family == AF_INET || $sa->sa_family == AF_INET6) {
printf("\n########## exit accept4() ##########\n");
printf("accept4: pid:%-6d comm:%-16s family:%-3d ", pid, comm, $sa->sa_family);
$error = args->ret;
if ($sa->sa_family == AF_INET) { //IPv4
$s = (struct sockaddr_in *)@sockaddr[tid];
$port = ($s->sin_port >> 8) |
(($s->sin_port << 8) & 0xff00);
printf("peerIP:%-16s peerPort:%-5d fd:%d\n",
ntop(AF_INET, $s->sin_addr.s_addr),
$port, $error);
printf("stack: %s\n", ustack);
} else { //IPv6
$s6 = (struct sockaddr_in6 *)@sockaddr[tid];
$port = ($s6->sin6_port >> 8) |
(($s6->sin6_port << 8) & 0xff00);
printf("%-16s %-5d %d\n",
ntop(AF_INET6, $s6->sin6_addr.in6_u.u6_addr8),
$port, $error);
printf("stack: %s\n", ustack);
}
}
delete(@sockaddr[tid]);
}
}
END
{
clear(@sockaddr);
clear(@fam2str);
}
現(xiàn)在開始行動,如果你看不懂為何如此,不要急,后面會解析為何:
- 啟動殼進程,以讓我們預(yù)先可以得到將啟動的 envoy 的 PID
$ bash -c '
echo "pid=$$";
echo "Any key execute(exec) envoy ..." ;
read;
exec ./envoy -c ./envoy-demo.yaml'
輸出:
pid=5678
Any key execute(exec) envoy ...
- 啟動跟蹤 bpftrace 腳本。在新的終端中執(zhí)行:
$ bpftrace trace-envoy-socket.bt 5678
- 回到步驟 1 的殼進程終端。按下空格鍵,Envoy 正式運行,PID 保持為 5678
- 這時,我們在運行 bpftrace 腳本的終端中看到跟蹤的準實時輸出結(jié)果:
$ bpftrace trace-envoy-socket.bt
########## 1.setsockopt() ##########
comm:envoy : setsockopt: fd=22, optname=2, optval=1, optlen=4. stack:
setsockopt+14
Envoy::Network::IoSocketHandleImpl::setOption(int, int, void const*, unsigned int)+90
Envoy::Network::NetworkListenSocket<Envoy::Network::NetworkSocketTrait<...)0> >::setPrebindSocketOptions()+50
...
Envoy::Server::ListenSocketFactoryImpl::createListenSocketAndApplyOptions()+114
...
Envoy::Server::ListenerManagerImpl::createListenSocketFactory(...)+133
...
Envoy::Server::Configuration::MainImpl::initialize(...)+2135
Envoy::Server::InstanceImpl::initialize(...)+14470
...
Envoy::MainCommon::MainCommon(int, char const* const*)+398
Envoy::MainCommon::main(int, char**, std::__1::function<void (Envoy::Server::Instance&)>)+67
main+44
__libc_start_main+243
########## 2.bind() ##########
comm:envoy : bind AF_INET: ip:0.0.0.0 port:10000 fd=22
stack:
bind+11
Envoy::Network::IoSocketHandleImpl::bind(std::__1::shared_ptr<Envoy::Network::Address::Instance const>)+101
Envoy::Network::SocketImpl::bind(std::__1::shared_ptr<Envoy::Network::Address::Instance const>)+383
Envoy::Network::ListenSocketImpl::bind(std::__1::shared_ptr<Envoy::Network::Address::Instance const>)+77
Envoy::Network::ListenSocketImpl::setupSocket(...)+76
...
Envoy::Server::ListenSocketFactoryImpl::createListenSocketAndApplyOptions()+114
...
Envoy::Server::ListenerManagerImpl::createListenSocketFactory(...)+133
Envoy::Server::ListenerManagerImpl::setNewOrDrainingSocketFactory...
Envoy::Server::ListenerManagerImpl::addOrUpdateListenerInternal(...)+3172
Envoy::Server::ListenerManagerImpl::addOrUpdateListener(...)+409
Envoy::Server::Configuration::MainImpl::initialize(...)+2135
Envoy::Server::InstanceImpl::initialize(...)+14470
...
Envoy::MainCommon::MainCommon(int, char const* const*)+398
Envoy::MainCommon::main(int, char**, std::__1::function<void (Envoy::Server::Instance&)>)+67
main+44
__libc_start_main+243
這時,模擬一個 client 端過來連接:
$ telnet localhost 10000
連接成功后,可以看到 bpftrace 腳本繼續(xù)輸出了:
########## 3.exit accept4() ##########
accept4: pid:219185 comm:wrk:worker_1 family:2 peerIP:127.0.0.1 peerPort:38686 fd:20
stack:
accept4+96
Envoy::Network::IoSocketHandleImpl::accept(sockaddr*, unsigned int*)+82
Envoy::Network::TcpListenerImpl::onSocketEvent(short)+216
std::__1::__function::__func<Envoy::Event::DispatcherImpl::createFileEvent(...)+65
Envoy::Event::FileEventImpl::assignEvents(unsigned int, event_base*)::$_1::__invoke(int, short, void*)+92
event_process_active_single_queue+1416
event_base_loop+1953
Envoy::Server::WorkerImpl::threadRoutine(Envoy::Server::GuardDog&, std::__1::function<void ()> const&)+621
Envoy::Thread::ThreadImplPosix::ThreadImplPosix(...)+19
start_thread+217
########## 4.setsockopt() ##########
comm:wrk:worker_1 : setsockopt: fd=20, optname=1, optval=1, optlen=4. stack:
setsockopt+14
Envoy::Network::IoSocketHandleImpl::setOption(int, int, void const*, unsigned int)+90
Envoy::Network::ConnectionImpl::noDelay(bool)+143
Envoy::Server::ActiveTcpConnection::ActiveTcpConnection(...)+141
Envoy::Server::ActiveTcpListener::newConnection(...)+650
Envoy::Server::ActiveTcpSocket::newConnection()+377
Envoy::Server::ActiveTcpSocket::continueFilterChain(bool)+107
Envoy::Server::ActiveTcpListener::onAcceptWorker(...)+163
Envoy::Network::TcpListenerImpl::onSocketEvent(short)+856
Envoy::Event::FileEventImpl::assignEvents(unsigned int, event_base*)::$_1::__invoke(int, short, void*)+92
event_process_active_single_queue+1416
event_base_loop+1953
Envoy::Server::WorkerImpl::threadRoutine(Envoy::Server::GuardDog&, std::__1::function<void ()> const&)+621
Envoy::Thread::ThreadImplPosix::ThreadImplPosix(...)+19
start_thread+217
########## 5.exit accept4() ##########
accept4: pid:219185 comm:wrk:worker_1 family:2 peerIP:127.0.0.1 peerPort:38686 fd:-11
stack:
accept4+96
Envoy::Network::IoSocketHandleImpl::accept(sockaddr*, unsigned int*)+82
Envoy::Network::TcpListenerImpl::onSocketEvent(short)+216
std::__1::__function::__func<Envoy::Event::DispatcherImpl::createFileEvent(...)+65
Envoy::Event::FileEventImpl::assignEvents(unsigned int, event_base*)::$_1::__invoke(int, short, void*)+92
event_process_active_single_queue+1416
event_base_loop+1953
Envoy::Server::WorkerImpl::threadRoutine(Envoy::Server::GuardDog&, std::__1::function<void ()> const&)+621
Envoy::Thread::ThreadImplPosix::ThreadImplPosix(...)+19
start_thread+217
如果你之前沒接觸過 bpftrace(相信大部分人是這種情況),你可以先猜想分析一下前面的信息,再看我下面的說明。
bpftrace 腳本分析
回到上面的 bpftrace 腳本 trace-envoy-socket.bt 。
可以看到有很多的 tracepoint:syscalls:sys_enter_xyz 函數(shù),每個其實都是一些鉤子方法,在進程調(diào)用 xzy 方法時,相應(yīng)的鉤子方法會被調(diào)用。而在鉤子方法中,可以分析 xyz 函數(shù)的入?yún)ⅰ⒎祷刂担ǔ鰠ⅲ?、?dāng)前線程的函數(shù)調(diào)用堆棧等信息。并可以把信息分析狀態(tài)保存在一個 BPF map 中。
在上面例子里,我們攔截了 setsockopt、bind、accept4(進入與返回),4個事件,并打印出相關(guān)入出參數(shù)、進程當(dāng)前線程的堆棧。
每個鉤子方法都有一個:/pid == $1/ 。它是個附加的鉤子方法調(diào)用條件。因 tracepoint 類型攔截點是對整個操作系統(tǒng)的,但我們只關(guān)心自己啟動的 envoy 進程,所以要加入 envoy 進程的 pid 作為過濾。其中 $1 是我們運行 bpftrace trace-envoy-socket.bt 5678 命令時的第 1 個參數(shù),即為 enovy 進程的 pid。
bpftrace 輸出結(jié)果分析
-
envoy 主線程設(shè)置了主監(jiān)聽 socket 的 setsockopt
comm:envoy。說明這是主線程
fd=22。 說明 socket 文件句柄為 22(每個socket都對應(yīng)一個文件句柄編號,相當(dāng)于 socket id)。
optname=2, optval=1。說明設(shè)置項id為 2(SO_REUSEADDR),値為 1。
setsockopt+14 到 __libc_start_main+243 為當(dāng)前線程的函數(shù)調(diào)用堆棧。通過這,可以對應(yīng)上項目源碼了。
-
envoy 主線程把主監(jiān)聽 socket 的綁定監(jiān)聽在 IP 0.0.0.0 的端口 10000 上,調(diào)用 bind
- comm:envoy。說明這是主線程
- fd=22。 說明 socket 文件句柄為 22,即和上一步是相同的 socket
- ip:0.0.0.0 port:10000。說明 socket 的監(jiān)聽地址
- 其它就是當(dāng)前線程的函數(shù)調(diào)用堆棧。通過這,可以對應(yīng)上項目源碼。
-
envoy 的 worker 線程之一的 wrk:worker_1 線程接受了一個新客戶端的連接。并 setsockopt
- comm:wrk:worker_1 。envoy 的 worker 線程之一的 wrk:worker_1 線程
- peerIP:127.0.0.1 peerPort:38686。說明新客戶端對端的地址。
- fd:20。 說明新接受的 socket 文件句柄為 20。
-
wrk:worker_1 線程 setsockopt 新客戶端 socket 連接
- fd:20。 說明新接受的 socket 文件句柄為 20。
- optname=1, optval=1。說明設(shè)置項id為 1(TCP_NODELAY),値為 1。
暫時忽略這個,這很可能是傳說中的 epoll 假 wakeup。
上面應(yīng)該算說得還清楚,但肯定要補充的是 setsockopt 中,設(shè)置項id的意義:
setsockopt 參數(shù)說明:
| level | optname | 描述名 | 描述 |
|---|---|---|---|
| IPPROTO_TCP=8 | 1 | TCP_NODELAY | 0: 打開 Nagle 算法,延遲發(fā) TCP 包<br />1:禁用 Nagle 算法 |
| SOL_SOCKET=1 | 2 | SO_REUSEADDR | 1:打開地址重用 |
通過這個跟蹤,我們實現(xiàn)了既定目標。同時可以看到線程函數(shù)調(diào)用堆棧,可以從我們選擇關(guān)注的埋點去分析 envoy 的實際行為。結(jié)合源碼分析運行期的程序行為。比光看靜態(tài)源碼更快和更有目標性地達成目標。特別是現(xiàn)代大項目大量使用的高級語言特性、OOP多態(tài)和抽象等技術(shù),有時候讓直接閱讀代碼去分析運行期行為和設(shè)計實際目的變得相當(dāng)困難。而有了這種技術(shù),會簡化這個困難。
展望
//TODO
細說逆向工程思維
這小節(jié)有點深。不是必須的知識,只是介紹一點背景,因篇幅問題也不可能說得清晰,要清晰直接看參考資料一節(jié)。本節(jié)不喜可跳過。勇敢如你能讀到這里,就不要被本段嚇跑了。
進程的內(nèi)存與可執(zhí)行文件的關(guān)系
可執(zhí)行文件格式
程序代碼被編譯和鏈接成包含二進制計算機指令的可執(zhí)行文件。而可執(zhí)行文件是有格式規(guī)范的,在 Linux 中,這個規(guī)范叫 Executable and linking format (ELF)。ELF 中包含二進制計算機指令、靜態(tài)數(shù)據(jù)、元信息。
- 靜態(tài)數(shù)據(jù) - 我們在程序中 hard code 的東西數(shù)據(jù),如字串常量等
- 二進制計算機指令集合,程序代碼邏輯生成的計算機指令。代碼中的每個函數(shù)都在編譯時生成一塊指令,而鏈接器負責(zé)把一塊塊指令連續(xù)排列到輸出的 ELF 文件的
.text section(區(qū)域)中。而元信息中的.symtab section(區(qū)域)記錄了每個函數(shù)在.text section的地址。說白了,就是代碼中的函數(shù)名到 ELF 文件地址或運行期進程內(nèi)存地址的 mapping 關(guān)系。.symtab section對我們逆向工程分析很有用。 - 元信息 - 告訴操作系統(tǒng),如何加載和動態(tài)鏈接可執(zhí)行文件,完成進程內(nèi)存的初始化。其中可以包括一些非運行期必須,但可以幫助定位問題的信息。如上面說的
.symtab section(區(qū)域)

Typical ELF executable object file.
From [Computer Systems - A Programmer’s Perspective]
進程的內(nèi)存
一般意義的進程是指可執(zhí)行文件運行實例。進程的內(nèi)存結(jié)構(gòu)可能大致劃分為:

Process virtual address space.
From [Computer Systems - A Programmer’s Perspective]
其中的 Memory-mapped region for shared libraries 是二進制計算機指令部分,可先簡單認為是直接 copy 或映射自可執(zhí)行文件的 .text section(區(qū)域) (雖然這不完全準確)。
計算機底層的函數(shù)調(diào)用
有時候不知是幸運還是不幸?,F(xiàn)在的程序員的程序視角和90年代時的大不相同。高級語言/腳本語言、OOP、等等都告訴程序員,你不需要了解底層細節(jié)。
但有時候了解底層細節(jié),才可以創(chuàng)造出通用共性的創(chuàng)新。如 kernel namespace 到 container,netfiler 到 service mesh。
回來吧,說說本文的重點函數(shù)調(diào)用。我們知道,高級語言的函數(shù)調(diào)用,其實絕大部分情況下會編譯成機器語言的函數(shù)調(diào)用,其中的堆棧處理和高級語言是相近的。
如以下一段代碼:
//main.c
void funcA() {
int a;
}
void main() {
int m;
funcA();
}
生成匯編:
gcc -S ./blogc.c
匯編結(jié)果片段:
funcA:
endbr64
pushq %rbp
movq %rsp, %rbp
nop
popq %rbp
ret
...
main:
endbr64
pushq %rbp
movq %rsp, %rbp
movl $0, %eax
call funcA <----- 調(diào)用 funcA
nop
popq %rbp
ret
即實際上,計算機底層也是有函數(shù)調(diào)用指令,內(nèi)存中也有堆棧內(nèi)存的概念。

堆棧在內(nèi)存中的結(jié)構(gòu)和 CPU 寄存器的引用
From [BPF Performance Tools]
所以,只要在代碼中埋點,分析當(dāng)前 CPU 寄存器的引用。加上分析堆棧的結(jié)構(gòu),就可以得到當(dāng)前線程的函數(shù)調(diào)用鏈。而當(dāng)前函數(shù)的出/入?yún)⒁彩欠湃肓酥付ǖ募拇嫫?。所以也可以探視到?入?yún)ⅰ>唧w原理可以看參考一節(jié)的內(nèi)容。
埋點
ebpf 工具的埋點的方法有很多,常用最少包括:
- uprobe 應(yīng)用函數(shù)埋點:參考:https://blog.mygraphql.com/zh/posts/low-tec/trace/trace-quick-start/#如何監(jiān)聽函數(shù)
- kprobe 內(nèi)核函數(shù)埋點
- tracepoint 內(nèi)核預(yù)定義事件埋點
- 硬件事件埋點:如異常(如內(nèi)存分頁錯誤)、CPU 事件(如 cache miss)
使用哪個還得參考 [BPF Performance Tools] 深入了解一下。
精彩的參考
- [Computer Systems - A Programmer’s Perspective - Third edition] - Randal E. Bryant ? David R. O’Hallaron - 一本用程序員、操作系統(tǒng)角度深入計算機原理的書。介紹了編譯和鏈接、程序加載、進程內(nèi)存結(jié)構(gòu)、函數(shù)調(diào)用堆棧等基本原理
- https://cs61.seas.harvard.edu/site/2018/Asm2/ - 函數(shù)調(diào)用堆棧等基本原理
- [Learning Linux Binary Analysis] - Ryan "elfmaster" O'Neill - ELF 格式深入分析和利用
- The ELF format - how programs look from the inside
- [BPF Performance Tools] - Brendan Gregg
卡脖子的現(xiàn)實的一點參考信息
卡脖子根本原因
根本原因類似 https://github.com/iovisor/bcc/issues/2648 。我可能以后寫文章詳述。
有沒函數(shù)元信息(.symtab)?
Evnoy 和 Istio Proxy 的 Release ELF 中,到底默認有沒函數(shù)元信息(.symtab)
https://github.com/istio/istio/issues/14331
Argh, we ship
envoybinary without symbols.Could you get the version of your istio-proxy by calling
/usr/local/bin/envoy --version? It should include commit hash. Since you're using 1.1.7, I believe the version output will be:version: 73fa9b1f29f91029cc2485a685994a0d1dbcde21/1.11.0-dev/Clean/RELEASE/BoringSSLOnce you have the commit hash, you can download
envoybinary with symbols from
https://storage.googleapis.com/istio-build/proxy/envoy-alpha-73fa9b1f29f91029cc2485a685994a0d1dbcde21.tar.gz (change commit hash if you have a different version of istio-proxy).You can use
gdbwith that binary, use it instead of/usr/local/bin/envoyand you should see more useful backtrace.Thanks!
@Multiply sorry, I pointed you at the wrong binary, it should be this one instead: https://storage.googleapis.com/istio-build/proxy/envoy-symblol-73fa9b1f29f91029cc2485a685994a0d1dbcde21.tar.gz (
symbol, notalpha).
envoy binary file size - currently 127MB #240: https://github.com/envoyproxy/envoy/issues/240mattklein123 commented on Nov 23, 2016
The default build includes debug symbols and is statically linked. If you strip symbols that's what takes you down to 8MB or so. If you want to go down further than that you should dynamically link against system libraries.FWIW, we haven't really focused very much on the build/package/install side of things. I'm hoping the community can help out there. Different deployments are going to need different kinds of compiles.
原文:
逆向工程思維解決云原生現(xiàn)場分析問題 Part1(預(yù)覽版本v3) —— eBPF 跟蹤 Istio/Envoy/K8S