逆向工程思維解決雲原生現場分析問題 Part1 —— eBPF 跟蹤 Istio/Envoy/K8S

MarkZhu發表於2022-02-11

image.png

緣起

雲原生複雜性

在 200x 年時代,服務端軟體架構,組成的複雜度,異構程度相對於雲原生,可謂簡單很多。那個年代,大多數基礎元件,要麼由使用企業開發,要麼是購買元件服務支援。

到了 201x 年代,開源運動,去 IOE 運動興起。企業更傾向選擇開源基礎元件。然而開源基礎的維護和問題解決成本其實並不是看起來那麼低。給你原始碼,你以為就什麼都看得透嗎?對於企業,現在起碼有幾個大問題:

從高處看:

  • 企業要投入多少人力才、財力可以找到或培養一個看得透開源基礎元件的人?
  • 開源的版本、安全漏洞、更迭快速,即使專業人才也很難快速看得透執行期的軟體行為。
  • 元件之間錯綜複雜的依賴、呼叫關係,再加上版本依賴和更迭,沒有可能執行過完全相同環境的測試(哪怕你用了vm/docker image)

    • 或者你還很迷戀向後相容,即使它已經傷害過無數程式設計師的心和夜晚
    • 就像 古希臘哲學家赫拉克利特說:no one can step into the same river once(人不能兩次踏進同一條河流)

從細節看:

  • 對於大型的開源專案,一般企業沒可能投入人力看懂全部程式碼(注意,是看懂,不是看過)。而企業真正關心或使用的,可能只是一小部分和切身故障相關的子模組。
  • 對於大型的開源專案,即使你認為看懂全部程式碼。你也不太可能瞭解全部執行期的狀態。哪怕是專案作者,也不一定可以。

    • 專案的作者不在企業,也不可能完全瞭解企業中資料的特性。更何況無處不在的 bug
  • 開源軟體的精神在於開放與 free(這裡不是指免費,這裡只能用英文),而 free 不單單是 read only,它還是 writable 的。

    • 開源軟體大都不是大公司中某天才產品經理、天才構架師設計出來。而是眾多使用者一起打磨出來的。但如果要看懂全部程式碼才能 writable,恐怕沒人可以修改 Linux 核心了。
  • 靜態的程式碼。這點我認為是最重要的。我們所謂的看懂全部程式碼,是指靜態的程式碼。但有經驗的程式設計師都知道,程式碼只有跑起來,才真正讓人看得通透。而能分析一個跑起來的程式,才可以說,我看懂全部程式碼。

    • 這讓我想起,一般的 code review,都在 review 什麼?

雲原生現場分析的難

賣了半天的關子,那麼有什麼方法可以賣弄?可以快速理點,分析開源專案執行期行為?

  1. 加日誌。

    1. 如果要解決的問題剛才原始碼中有日誌,或者提供日誌開關,當然就開啟完事。收工開飯。但這運氣得多好?
    2. 修改開源原始碼,加入日誌,來個緊急上線。這樣你得和運維關係有多鐵?你確定加一次就夠了嗎?
  2. 語言級別的動態 instrumentation 注入程式碼

    1. 在注入程式碼中分析資料或出日誌。如 alibaba/arthas 。golang instrumentation
    2. 這對語言有要求,如果是 c/c++ 等就 愛莫能助 了。
    3. 對效能影響一般也不少。
  3. debug

    1. java debug / golang Delve / gdb 等,都有一定的使用門檻,如程式打包時需要包含了 debug 資訊。這在當下喜歡計較 image 大小的年代,debug 資訊多被翦掉。同時,斷點時可能掛起執行緒甚至整個程式。生產環境上發生就是災難。
  4. uprobe/kprobe/eBPF

    1. 在上面方法都不可行時,這個方法值得一試。下面,我們分析一下,什麼是 uprobe/kprobe/eBPF。為何有價值。

逆向工程思維

我們知道現在大部分程式都是用高階語言編碼,再編譯生成可執行的檔案( .exe / ELF ) 或中間檔案在執行期 JIT 編譯。最終一定要生成計算機指令,計算機才能執行。對於開源專案,如果我們找到了這堆生成的計算機指令和原始碼之間對映關係。然後:

  1. 在這堆計算機指令的一個合理的位置(可以先假設這個位置就是我們關注的一個高階語言函式的入口)中放入一個鉤子
  2. 如果程式執行到鉤子時,我們可以探視:

    1. 當前程式的函式呼叫堆疊
    2. 當前函式呼叫的引數、返回值
    3. 當前程式的靜態/全域性變數

對於開源專案,知道執行期的實際狀態是現場分析問題解決的關鍵。

由於不想讓本文開頭過於理論,嚇跑人,我把 細說逆向工程思維 一節移到最後。

實踐

我之前寫技術文章很少寫幾千字還沒一行程式碼。不過最近不知道是年紀漸長,還是怎的,總想多說點廢話。

Show me the code.

實踐目標

我們探視所謂的雲原生服務網格之背骨的 Envoy sidecar 代理為例子,看看 Envoy 啟動過程和建立客戶端連線過程中:

  1. 是在什麼程式碼去監聽 TCP 埠
  2. 監聽的 socket 是否設定了中外馳名的 SO_REUSEADDR
  3. TCP 連線又是否啟用了臭名昭著的增大網路時延的 Nagle 演算法(還是相反 socket 設定了 TCP_NODELAY),見 https://en.wikipedia.org/wiki...

說了那麼多廢話,主角來了,eBPF技術和我們這次要用的工具 bpftrace。

先說說我的環境:

  • Ubuntu Linux 20.04
  • 系統預設的 bpftrace v0.9.4 (這版本有問題,後面說)

Hello World

上面的 3 實踐目標很“偉大”。但我們在實現前,還是先來個小目標,寫個 Hello World 吧。

我們知道 envoy 原始碼的主入口在 main_common.cc 的:

int MainCommon::main(int argc, char** argv, PostServerHook hook) {
    ...
}

我們目標是在 envoy 初始化時,呼叫這個函式時輸出一行資訊,代表成功攔截。

首先看看 envoy 可執行檔案中帶有的函式地址元資訊:

➜  ~ readelf -s --wide ./envoy | egrep 'MainCommon.*main'                                                       
114457: 00000000016313c0   635 FUNC    GLOBAL DEFAULT   14 _ZN5Envoy10MainCommon4mainEiPPcNSt3__18functionIFvRNS_6Server8InstanceEEEE

這裡需要說明一下,c++ 程式碼編譯時,內部表示函式的名字不是直接使用原始碼的名字,是規範化變形(mangling)後的名字(可以用 c++filt 命令手工轉換)。這裡我們得知變形後的函式名是:_ZN5Envoy10MainCommon4mainEiPPcNSt3__18functionIFvRNS_6Server8InstanceEEEE。於是可以用 bpftrace去攔截了。

bpftrace -e 'uprobe:./envoy:_ZN5Envoy10MainCommon4mainEiPPcNSt3__18functionIFvRNS_6Server8InstanceEEEE { printf("Hello world: Got MainCommon::main"); }'

這時,在另外一個終端中執行 envoy

./envoy -c envoy-demo.yaml

卡脖子的現實

在我初學攝影時,老師告訴我一個情況叫:Beginner's luck。而技術界往往相反。這次,我什麼都沒攔截到。用自以為是的經驗摸索了各種方法,均無果。我在這種摸索、無果的迴圈中折騰了大概半年……

突破

折騰了大概半年後,我實在想放棄了。想不到,一個 Hello World 小目標也完成不了。直到一天,我醒悟到說到底是自己基礎知識不好,才不能定位到問題的根源。於是惡補了 程式連結、ELF檔案格式、ELF 載入程式記憶體 等知識。後來,千辛萬苦最於找到根本原因(如果一定要一句話說完,就是 bpftrace 舊版本錯誤解釋了函式元資訊的地址 )。相關的細節我將寫成一編獨立的技術文章。這裡先不多說。解決方法卻很簡單,升級 bpftrace,我直接自己編譯了 bpftrace v0.14.1 。

終於,在啟動 envoy 後輸出了:

Hello world: Got MainCommon::main
^C

實踐

我嘗試不按正常的順序思維講這部分。因為一開始去分析實現原理,指令碼程式,還不如先瀏覽一下程式碼,然後執行一次給大家看。

我們先簡單瀏覽 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);
}

現在開始行動,如果你看不懂為何如此,不要急,後面會解析為何:

  1. 啟動殼程式,以讓我們預先可以得到將啟動的 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 ...
  1. 啟動跟蹤 bpftrace 指令碼。在新的終端中執行:
$ bpftrace trace-envoy-socket.bt 5678
  1. 回到步驟 1 的殼程式終端。按下空格鍵,Envoy 正式執行,PID 保持為 5678
  2. 這時,我們在執行 bpftrace 指令碼的終端中看到跟蹤的準實時輸出結果:
$ 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 指令碼繼續輸出了:

########## 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 函式,每個其實都是一些鉤子方法,在程式呼叫 xzy 方法時,相應的鉤子方法會被呼叫。而在鉤子方法中,可以分析 xyz 函式的入參、返回值(出參)、當前執行緒的函式呼叫堆疊等資訊。並可以把資訊分析狀態儲存在一個 BPF map 中。

在上面例子裡,我們攔截了 setsockopt、bind、accept4(進入與返回),4個事件,並列印出相關入出引數、程式當前執行緒的堆疊。

每個鉤子方法都有一個:/pid == $1/ 。它是個附加的鉤子方法呼叫條件。因 tracepoint 型別攔截點是對整個作業系統的,但我們只關心自己啟動的 envoy 程式,所以要加入 envoy 程式的 pid 作為過濾。其中 $1 是我們執行 bpftrace trace-envoy-socket.bt 5678 命令時的第 1 個引數,即為 enovy 程式的 pid。

bpftrace 輸出結果分析

  1. envoy 主執行緒設定了主監聽 socket 的 setsockopt

    • comm:envoy。說明這是主執行緒
    • fd=22。 說明 socket 檔案控制程式碼為 22(每個socket都對應一個檔案控制程式碼編號,相當於 socket id)。
    • optname=2, optval=1。說明設定項id為 2(SO_REUSEADDR),値為 1。
    • setsockopt+14 到 __libc_start_main+243 為當前執行緒的函式呼叫堆疊。通過這,可以對應上專案原始碼了。
  2. envoy 主執行緒把主監聽 socket 的繫結監聽在 IP 0.0.0.0 的埠 10000 上,呼叫 bind

    • comm:envoy。說明這是主執行緒
    • fd=22。 說明 socket 檔案控制程式碼為 22,即和上一步是相同的 socket
    • ip:0.0.0.0 port:10000。說明 socket 的監聽地址
    • 其它就是當前執行緒的函式呼叫堆疊。通過這,可以對應上專案原始碼。
  3. 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。
  4. wrk:worker_1 執行緒 setsockopt 新客戶端 socket 連線

    • fd:20。 說明新接受的 socket 檔案控制程式碼為 20。
    • optname=1, optval=1。說明設定項id為 1(TCP_NODELAY),値為 1。
  5. 暫時忽略這個,這很可能是傳說中的 epoll 假 wakeup。

上面應該算說得還清楚,但肯定要補充的是 setsockopt 中,設定項id的意義:

setsockopt 引數說明:

leveloptname描述名描述
IPPROTO_TCP=81TCP_NODELAY0: 開啟 Nagle 演算法,延遲發 TCP 包
1:禁用 Nagle 演算法
SOL_SOCKET=12SO_REUSEADDR1:開啟地址重用

通過這個跟蹤,我們實現了既定目標。同時可以看到執行緒函式呼叫堆疊,可以從我們選擇關注的埋點去分析 envoy 的實際行為。結合原始碼分析執行期的程式行為。比光看靜態原始碼更快和更有目標性地達成目標。特別是現代大專案大量使用的高階語言特性、OOP多型和抽象等技術,有時候讓直接閱讀程式碼去分析執行期行為和設計實際目的變得相當困難。而有了這種技術,會簡化這個困難。

展望

//TODO

細說逆向工程思維

這小節有點深。不是必須的知識,只是介紹一點背景,因篇幅問題也不可能說得清晰,要清晰直接看參考資料一節。本節不喜可跳過。勇敢如你能讀到這裡,就不要被本段嚇跑了。

程式的記憶體與可執行檔案的關係

可執行檔案格式

程式程式碼被編譯和連結成包含二進位制計算機指令的可執行檔案。而可執行檔案是有格式規範的,在 Linux 中,這個規範叫 Executable and linking format (ELF)。ELF 中包含二進位制計算機指令、靜態資料、元資訊。

  • 靜態資料 - 我們在程式中 hard code 的東西資料,如字串常量等
  • 二進位制計算機指令集合,程式程式碼邏輯生成的計算機指令。程式碼中的每個函式都在編譯時生成一塊指令,而連結器負責把一塊塊指令連續排列到輸出的 ELF 檔案的 .text section(區域) 中。而元資訊中的.symtab section(區域) 記錄了每個函式在 .text section 的地址。說白了,就是程式碼中的函式名到 ELF 檔案地址或執行期程式記憶體地址的 mapping 關係。.symtab section 對我們逆向工程分析很有用。
  • 元資訊 - 告訴作業系統,如何載入和動態連結可執行檔案,完成程式記憶體的初始化。其中可以包括一些非執行期必須,但可以幫助定位問題的資訊。如上面說的 .symtab section(區域)

image.png

Typical ELF executable object file.
From [Computer Systems - A Programmer’s Perspective]

程式的記憶體

一般意義的程式是指可執行檔案執行例項。程式的記憶體結構可能大致劃分為:

image.png
Process virtual address space.From [Computer Systems - A Programmer’s Perspective]

其中的 Memory-mapped region for shared libraries 是二進位制計算機指令部分,可先簡單認為是直接 copy 或對映自可執行檔案的 .text section(區域) (雖然這不完全準確)。

計算機底層的函式呼叫

有時候不知是幸運還是不幸。現在的程式設計師的程式視角和90年代時的大不相同。高階語言/指令碼語言、OOP、等等都告訴程式設計師,你不需要了解底層細節。

但有時候瞭解底層細節,才可以創造出通用共性的創新。如 kernel namespace 到 container,netfiler 到 service mesh。

回來吧,說說本文的重點函式呼叫。我們知道,高階語言的函式呼叫,其實絕大部分情況下會編譯成機器語言的函式呼叫,其中的堆疊處理和高階語言是相近的。

如以下一段程式碼:

//main.c

void funcA() {
    int a;
}

void main() {
    int m;
    funcA();
}

生成彙編:

gcc -S ./blogc.c

彙編結果片段:

funcA:
    endbr64
    pushq    %rbp
    movq    %rsp, %rbp
    nop
    popq    %rbp
    ret
...


main:
    endbr64
    pushq    %rbp
    movq    %rsp, %rbp
    movl    $0, %eax
    call    funcA <----- 呼叫 funcA
    nop
    popq    %rbp
    ret

即實際上,計算機底層也是有函式呼叫指令,記憶體中也有堆疊記憶體的概念。

image.png

堆疊在記憶體中的結構和 CPU 暫存器的引用 From [BPF Performance Tools]

所以,只要在程式碼中埋點,分析當前 CPU 暫存器的引用。加上分析堆疊的結構,就可以得到當前執行緒的函式呼叫鏈。而當前函式的出/入參也是放入了指定的暫存器。所以也可以探視到出/入參。具體原理可以看參考一節的內容。

埋點

ebpf 工具的埋點的方法有很多,常用最少包括:

使用哪個還得參考 [BPF Performance Tools] 深入瞭解一下。

精彩的參考

  • [Computer Systems - A Programmer’s Perspective - Third edition] - Randal E. Bryant • David R. O’Hallaron - 一本用程式設計師、作業系統角度深入計算機原理的書。介紹了編譯和連結、程式載入、程式記憶體結構、函式呼叫堆疊等基本原理
  • https://cs61.seas.harvard.edu... - 函式呼叫堆疊等基本原理
  • [Learning Linux Binary Analysis] - Ryan "elfmaster" O'Neill - ELF 格式深入分析和利用
  • The ELF format - how programs look from the inside
  • [BPF Performance Tools] - Brendan Gregg

卡脖子的現實的一點參考資訊

卡脖子根本原因

根本原因類似 https://github.com/iovisor/bc... 。我可能以後寫文章詳述。

有沒函式元資訊(.symtab)?

Evnoy 和 Istio Proxy 的 Release ELF 中,到底預設有沒函式元資訊(.symtab)

https://github.com/istio/isti...

Argh, we ship envoy binary 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/BoringSSL

Once you have the commit hash, you can download envoy binary with symbols from
https://storage.googleapis.co... (change commit hash if you have a different version of istio-proxy).

You can use gdb with that binary, use it instead of /usr/local/bin/envoy and 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.co... (symbol, not alpha).
envoy binary file size - currently 127MB #240: https://github.com/envoyproxy...

mattklein123 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.

原文:
https://blog.mygraphql.com/zh/posts/low-tec/trace/trace-istio/trace-istio-part1/

相關文章