39

gops 工作原理

 4 years ago
source link: https://www.tuicool.com/articles/nEJBFvy
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

网上已经有一篇《 gops 工作原理 》了,但我觉得其阐之未尽,想以自己的理解来讲述一下。

gops 是什么

gops (Go Process Status) 是 Google 出品的一个命令行工具,类似于 linux 自带的 ps 命令,gops 命令用于显示当前系统中 go 开发的程序的进程状态。形如:

$ gops
983   980    uplink-soecks  go1.9   /usr/local/bin/uplink-soecks
52697 52695  gops           go1.10  /Users/jbd/bin/gops
4132  4130   foops        * go1.9   /Users/jbd/bin/foops
51130 51128  gocode         go1.9.2 /Users/jbd/bin/gocode

指定进程号可以显示更详细的信息:

$ gops <pid>
parent PID:	5985
threads:	27
memory usage:	0.199%
cpu usage:	0.139%
username:	jbd
cmd+args:	/Applications/Splice.app/Contents/Resources/Splice Helper.app/Contents/MacOS/Splice Helper -pid 5985
local/remote:	127.0.0.1:56765 <-> :0 (LISTEN)
local/remote:	127.0.0.1:56765 <-> 127.0.0.1:50955 (ESTABLISHED)
local/remote:	100.76.175.164:52353 <-> 54.241.191.232:443 (ESTABLISHED)

目前为止,上述功能都是非侵入式的,golang 程序无需添加额外的代码或配置,gops 便能正确地识别出它们,并显示进程信息。

然而,如果愿意做一点“小小的牺牲”,加一点侵入式的代码,即引入一个 agent,形如:

package main

import (
	"log"
	"time"

	"github.com/google/gops/agent"
)

func main() {
	if err := agent.Listen(agent.Options{}); err != nil {
		log.Fatal(err)
	}
	time.Sleep(time.Hour)
}

那么,程序对应的进程就会在 gops 的显示列表中额外标记一个 * ,例如上述的:

4132  4130   foops        * go1.9   /Users/jbd/bin/foops

对于被标记 * 的进程,可以启用更强大的诊断功能,包括但不仅限于堆栈分析、内存分析、GC分析等。

那么,问题来了,gops 是如何工作的?它如何识别出哪些进程是 golang 开发的程序?

gops 的原理

首先排除一下干扰,对于引入了 agent 的情况,原理是很容易想到的:agent 作为“内鬼”,将程序运行中的各项信息输出来,既可以写入到文件方便本地访问,也可输出到端口方便远程访问。具体的实现虽然复杂,但理解起来并不困难。所以这里不对其做深入的探讨,只需要知道在具体实现上,引入了 agent 的程序会将当前运行信息写入到路径为 {$GOPS_CONFIG_DIR}/{$PID}{$HOME}/.config/gops/{$PID} 的目录下,自然,当 gops 命令想对某进程做深入诊断,检查有没有目标进程号对应的文件夹,并读取其中保存的信息即可。

排除了这个干扰,现在问题缩小为,在没有 agent 帮忙里应外合的情况下,gops 如何知道当前系统有哪些 golang 的进程?要知道,对于操作系统而言,golang 进程与其他进程并没有什么两样,操作系统既不区别对待,也不知道如何区别。

那操作系统知道啥?作为操作系统,有一项功能是必然要向用户或用户程序提供的,即告知当前系统有多少进程以及所有进程的运行信息(当然在权限满足的情况下),比如 linux(事实上我也只知道 linux)会将进程的所有运行信息按照特定结构写入 /proc 目录下,参阅 GNU/Linux下的/proc/[pid]目录下的文件分析

那么现在可以确认,gops 可以通过扫描 /proc 知道所有进程号,在通过读 /proc/[pid] 目录下的文件,就可以知道该进程的所有信息(事实上这也是 ps 的原理)。现在问题进一步缩小:gops 如何确认一个进程是 golang 进程甚至知道其 golang 版本号的?

其实想到这里也很容易想通了,还能怎么办,通过进程信息确认可执行文件位置,读可执行文件呗,可执行文件作为 golang 编译器的编译结果,肯定会留有一些信息指示编译器版本,可以通过 gdb 来证实:

$ gdb gops
……
(gdb) p 'runtime.buildVersion'
$1 = {
  str = 0x5c90ac "go1.10.2infinityinvalid loopbackmemstatsno anodereadfromreadlinkrecvfromresponserunnableruntime.scavengesendfilesignal: socket:[strconv.timeout:unixgramunknown( (forced) -> node= blocked= defersc= in "..., len = 8}
(gdb) q

$ gdb obfs-server
……
(gdb) p 'runtime.buildVersion'
No symbol "runtime.buildVersion" in current context.
(gdb) q

可确认文件 gops 是 golang 程序,编译器版本为 go1.10.2。而 obfs-server 不是 golang 程序。

至此,我们可以确认, gops 的工作原理是通过扫描系统当前所有的进程的可执行文件,确认是否为 golang 程序并获取编译器版本,对于 golang 程序进程则进一步检查 {$GOPS_CONFIG_DIR}/{$PID}{$HOME}/.config/gops/{$PID} 下是否有对应文件,如果有则说明该进程引入了 agent,则在显示该进程时添加额外标记。gops 从操作系统接口获取进程的运行信息,从 agent 输出的文件中读取 golang runtime 信息。

相关代码可以在 gops/goprocess/gp.go 中看到。

最后

最后还有一个问题,有没有可能程序是 golang 写的,但是 gops 却没有发现呢?当然是可能的。 如果 golang 程序编译时刻意抹去了编译信息,或者对编译结果加壳,gops 便可能无法正常工作

做个小实验说明一下,现有一个 golang 程序,代码为:

package main

import "time"

func main() {
        time.Sleep(time.Hour)
}

正常编译并运行,此时 gops 可以正常工作:

$ go build -o out main.go
$ ./out &
[1] 32227
$ gops
32227 29900 out                 go1.10.2           /root/test/out
32235 29900 gops                go1.10.2           /root/go/bin/gops
$ kill 32227

编译时指示删除调试符号,gops 则无法确认编译器版本,但仍能识别出是 golang 程序:

$ go build -ldflags "-s -w" -o out main.go
$ ./out &
[1] 32493
$ gops
32493 29900 out                 unknown Go version /root/test/out
32505 29900 gops                go1.10.2           /root/go/bin/gops
$ kill 32493

正常编译,编译结果再使用 upx 加壳,gops 已经完全不能识别出为 golang 程序了:

$ go build -o out main.go
$ upx out
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2017
UPX 3.94        Markus Oberhumer, Laszlo Molnar & John Reiser   May 12th 2017

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
   1179122 ->    440232   37.34%   linux/amd64   out

Packed 1 file.
$ ./out &
[1] 32744
$ gops
32763 29900 gops                go1.10.2           /root/go/bin/gops
$ kill 32744

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK