问题
我叫王大X,事情是这样的,我在一个服务内引入了net/http/pprof包,然后用http的默认handler开启了一个http的服务器,想要看下服务运行的pprof,大概如下
package main
import (
"context"
"log"
"net/http"
"net/http/pprof"
"os"
"os/signal"
"strconv"
"syscall"
)
var _ = pprof.Index
var httpServer *http.Server
type PProfConf struct {
Port int
}
func InitPProf(pprofConf PProfConf) {
if pprofConf.Port > 0 {
go func() {
httpServer = &http.Server{Addr: ":" + strconv.Itoa(pprofConf.Port), Handler: nil}
log.Print("try start pprof, listen on :", pprofConf.Port)
signalChan := make(chan os.Signal)
signal.Notify(signalChan, syscall.SIGTERM, syscall.SIGUSR2, syscall.SIGSEGV, syscall.SIGKILL)
go func() {
err := httpServer.ListenAndServe()
if err != nil {
log.Print("init pprof error: " + err.Error())
signalChan <- syscall.SIGKILL
}
}()
<-signalChan
log.Print("now stop http svr")
httpServer.Shutdown(context.Background())
log.Print("stop http successfully")
}()
}
}
复制代码
因为net/http/pprof
包默认会注册pprof相关的路由,理论上只要引入了这个包,然后开启http服务器就能使用pprof了, 注册路由的代码在net/http/pprof/pprof.go:80
可就当我想要访问的时候,万万没想到,404了。。。
> curl -GET http://localhost:16339/debug/pprof/
404 page not found
复制代码
而服务是成功监听了http端口的,好家伙。。。
定位过程
先是想想有什么可能的原因,因为net/http/pprof
包是在func init()
中注册路由的,也就是说,注册路由之后,可能在其他文件的init()方法,对http做了点什么手脚导致我访问不到。
而我怀疑有以下可能
- 服务里开启的http服务器,用的handler不是默认的http处理器
http.DefaultServeMux
- 路由加了莫名其妙的前缀
- 路由被注销了
第一个猜想比较容易认证,既然认为handler不对,那用http.DefaultServeMux
显式注册某个路由就好了,而事实证明,http服务器用的handler就是http.DefaultServeMux
httpServer = &http.Server{Addr: ":" + strconv.Itoa(pprofConf.Port), Handler: nil}
http.DefaultServeMux.HandleFunc("/debug/pprof/", pprof.Index)
log.Print("try start pprof, listen on :", pprofConf.Port)
signalChan := make(chan os.Signal)
// ... 以下省略
复制代码
这里显式注册的路由访问成功了,排除。
而二和三就难搞一点点,主要是http.DefaultServeMux
不支持把注册的路由打印出来,而且看了下http
的源码,也没有对外暴露方法,以支持对注册过的路由进行修改。
到这里加日志和看源码就解决不了话,就需要引入高级调试工具了,比如在Go slice扩容深度分析中用到的gdb。而我在这里用的是Delve,delve是专门为Go设计开发的调试工具,很好使。。可以看官方的github,当然还有巨人的肩膀。
使用delve来定位
既然是路由的问题,那就直接把断点打到注册的地方http.HandleFunc
,毕竟如果让我出于什么考量对路由做点什么手脚,我也会调回这里注册点我自己的路由,开搞
# 在程序目录下执行
$ dlv debug
Type 'help' for list of commands.
# 运行程序
(dlv) r
Process restarted with PID 24891
# 在注册路由的地方打上断点
(dlv) b /usr/local/Cellar/go/1.16.3/libexec/src/net/http/server.go:2497
Breakpoint 1 set at 0x16d04ef for net/http.(*ServeMux).HandleFunc() /usr/local/Cellar/go/1.16.3/libexec/src/net/http/server.go:2497
# 继续往下走
(dlv) c
> net/http.(*ServeMux).HandleFunc() /usr/local/Cellar/go/1.16.3/libexec/src/net/http/server.go:2497 (hits goroutine(1):1 total:1) (PC: 0x16d04ef)
2492: es[i] = e
2493: return es
2494: }
2495:
2496: // HandleFunc registers the handler function for the given pattern.
=>2497: func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
2498: if handler == nil {
2499: panic("http: nil handler")
2500: }
2501: mux.Handle(pattern, HandlerFunc(handler))
2502: }
# 打印路由的参数,以及堆栈信息,可以看到pprof的路由注册进去了
(dlv) p pattern
"/debug/pprof/"
(dlv) bt
0 0x00000000016d04ef in net/http.(*ServeMux).HandleFunc
at /usr/local/Cellar/go/1.16.3/libexec/src/net/http/server.go:2497
1 0x00000000016d05cb in net/http.HandleFunc
at /usr/local/Cellar/go/1.16.3/libexec/src/net/http/server.go:2513
2 0x0000000001811995 in net/http/pprof.init.0
at /usr/local/Cellar/go/1.16.3/libexec/src/net/http/pprof/pprof.go:84
3 0x0000000001051dff in runtime.doInit
# 这里省略一部分无用的堆栈,直接到关键信息
.....
# 然后就真的发现了一个奇怪的路由!!!
(dlv) p pattern
"/cmds"
# 打印堆栈,发现是一个叫trpc-go的家伙注册的
(dlv) bt
0 0x00000000016d04ef in net/http.(*ServeMux).HandleFunc
at /usr/local/Cellar/go/1.16.3/libexec/src/net/http/server.go:2497
1 0x000000000181a4af in git.code.oa.com/trpc-go/trpc-go/admin.(*router).Config
at /Users/siasyliang/go/pkg/mod/git.code.oa.com/trpc-go/trpc-go@v0.5.2/admin/router.go:49
2 0x0000000001816afa in git.code.oa.com/trpc-go/trpc-go/admin.init.0
at /Users/siasyliang/go/pkg/mod/git.code.oa.com/trpc-go/trpc-go@v0.5.2/admin/admin.go:45
3 0x0000000001051dff in runtime.doInit
at /usr/local/Cellar/go/1.16.3/libexec/src/runtime/proc.go:6265
4 0x0000000001051d6b in runtime.doInit
复制代码
于是用IDE追踪到代码,结果发现在admin.go
这个文件,发现有这么一个东西,因为安全方面的考量,直接帮我把路由给干掉了,真就好家伙了。。。
pprof的路由居然还真的是被干掉的!
知其所以然
上面也提到,http.DefaultServeMux
根本没有对外提供对已注册路由进行修改的方法,那是怎么做的呢?
可能已经有大哥想到了,没错,就是反射+unsafe。通过对http.DefaultServeMux
做反射,根据fieldName拿到路由对应的field,然后用unSafe.Pointer
拿到其指针,将其指针强转成map,并对map中的路由做delete操作,大概如下
v := reflect.ValueOf(http.DefaultServeMux)
// 删除map中的值
mField := v.Elem().FieldByName("m")
if !mField.IsValid() {
return errors.New("http.DefaultServeMux does not have a field called `m`")
}
mPointer := unsafe.Pointer(mField.UnsafeAddr())
m := (*map[string]muxEntry)(mPointer)
for _, pattern := range patterns {
delete(*m, pattern)
}
复制代码
于是就没有一点点防备,也没有一丝顾虑,就这样把我注册的pprof路由给干掉了,讲道理干掉也不输出点啥信息么。。。有点离谱
总结
笔者注册了pprof,但pprof访问不通
使用delve进行定位之后
发现有个包在init()方法中
使用反射+unsafe的办法把http.DefaultServeMux
中注册过的pprof相关的路由全delete掉了。
学到了吧?再被问到能不能访问到struct里的私有变量,即便没有对外暴露方法,也记得回答下反射+unsafe。
附代码里提到的安全问题:github.com/golang/go/i…
欢迎交流~