乐趣区

关于istio:逆向工程思维解决云原生现场分析问题-Part1-eBPF-跟踪-IstioEnvoyK8S

缘起

云原生复杂性

在 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 参数阐明:

level optname 形容名 形容
IPPROTO_TCP=8 1 TCP_NODELAY 0:关上 Nagle 算法,提早发 TCP 包
1:禁用 Nagle 算法
SOL_SOCKET=1 2 SO_REUSEADDR 1:关上地址重用

通过这个跟踪,咱们实现了既定目标。同时能够看到线程函数调用堆栈,能够从咱们抉择关注的埋点去剖析 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(区域)

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

过程的内存

个别意义的过程是指可执行文件运行实例。过程的内存构造可能大抵划分为:


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

即实际上,计算机底层也是有函数调用指令,内存中也有堆栈内存的概念。

堆栈在内存中的构造和 CPU 寄存器的援用 From [BPF Performance Tools]

所以,只有在代码中埋点,剖析以后 CPU 寄存器的援用。加上剖析堆栈的构造,就能够失去以后线程的函数调用链。而以后函数的出 / 入参也是放入了指定的寄存器。所以也能够探视到出 / 入参。具体原理能够看参考一节的内容。

埋点

ebpf 工具的埋点的办法有很多,罕用起码包含:

  • uprobe 利用函数埋点:参考:https://blog.mygraphql.com/zh/posts/low-tec/trace/trace-quick-start/# 如何监听函数
  • kprobe 内核函数埋点
  • tracepoint 内核预约义事件埋点
  • 硬件事件埋点:如异样(如内存分页谬误)、CPU 事件(如 cache miss)

应用哪个还得参考 [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/

退出移动版