「GCTT 出品」在 go 中如何調用私有函數(綁定隱藏的標識符)

2021-01-11 Go中國

2016 年 4 月 28 日

名字在 golang 中的重要性和在其他任何一種語言是一樣的。他們甚至含有語義的作用:在一個包的外部某個名字的可見性是由這個名字首字母是否是大寫來決定的。

有時為了更好的組織代碼或者在其他包使用某些隱藏的函數時需要克服這種限制。

在過去美好的日子,有 2 種實現方式,它們能繞過編譯器的檢查:不能引用未導出的名稱 pkg.symbol :

舊的方式,現在已經不再使用 - 彙編級隱式連接到所需符號,稱為 assembly stubs ,詳見 go runtime, os/signal: use //go:linkname instead of assembly stubs to get access to runtime functions 。現行的方式 - go 編譯器通過 go:linkname 支持名稱重定向,引用於 11.11.14 dev.cc code review 169360043: cmd/gc: changes for removing runtime C code (issue 169360043 by r…@golang.org) ,在 github.com 的 issue 上有可以找到 cmd/compile: 「missing function body」 error when using the //go:linkname compiler directive #15006 。用這些技巧我曾設法綁定 golang 運行時調度器相關的函數用以減少過度使用 go 的協程和內部鎖機制導致的 gc 停頓。

使用 assembly stubs

想法很簡單 - 為需要的標識符提供直接跳轉彙編指令 stubs 。連結器並不知道標識符是否已導出。

詳見舊版的代碼 src/os/signal/sig.s :

// Assembly to get into package runtime without using exported symbols.// +build amd64 amd64p32 arm arm64 386 ppc64 ppc64le#include "textflag.h"#ifdef GOARCH_arm#define JMP B#endif#ifdef GOARCH_ppc64#define JMP BR#endif#ifdef GOARCH_ppc64le#define JMP BR#endifTEXT ·signal_disable(SB),NOSPLIT,$0JMP runtime·signal_disable(SB)TEXT ·signal_enable(SB),NOSPLIT,$0JMP runtime·signal_enable(SB)TEXT ·signal_ignore(SB),NOSPLIT,$0JMP runtime·signal_ignore(SB)TEXT ·signal_recv(SB),NOSPLIT,$0JMP runtime·signal_recv(SB)

而 signal_unix.go 的綁定如下:

// +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris windowspackage signalimport ( "os" "syscall")// In assembly.func signal_disable(uint32)func signal_enable(uint32)func signal_ignore(uint32)func signal_recv() uint32

使用 go:linkname

為了使用這種方法,代碼中必須 import _ "unsafe" 包。為了解決 go 編譯器 -complete 參數的限制,一種可能的方法是在 main 包目錄加一個空的彙編 stub 文件以禁用編譯器的檢查。

詳見 os/signal/sig.s:

// The runtime package uses //go:linkname to push a few functions into this// package but we still need a .s file so the Go tool does not pass -complete// to the go tool compile so the latter does not complain about Go functions// with no bodies.

這個指令的格式是 //go:linkname localname linkname。使用這種方法可以將新的標識符連結(導出)或綁定到已存在的標識符(導入)。

用 go:linkname 導出

在 runtime/proc.go 中一個函數的實現

...//go:linkname sync_runtime_doSpin sync.runtime_doSpin//go:nosplitfunc sync_runtime_doSpin() { procyield(active_spin_cnt)}

這裡明確地向編譯器指示,將另一個名字添加到 sync 包的 runtime_doSpin 函數代碼中。並且 sync 包簡單的重用了在 sync/runtime.go 中的代碼:

package syncimport "unsafe"...// runtime_doSpin does active spinning.func runtime_doSpin()

用 go:linkname 導入

在 net/parse.go 中有一個很好的例子:

package netimport ( ... _ "unsafe" // For go:linkname)...// byteIndex is strings.IndexByte. It returns the index of the// first instance of c in s, or -1 if c is not present in s.// strings.IndexByte is implemented in runtime/asm_$GOARCH.s//go:linkname byteIndex strings.IndexBytefunc byteIndex(s string, c byte) int

使用這種技巧的方法:

import _ "unsafe" 包。

提供一個沒有函數體的函數,比如: func byteIndex(s string, c byte) int

在定義函數前,正確的放上 //go:linkname 指令,例如 //go:linkname byteIndex strings.IndexByte,byteIndex 是本地名稱,strings.IndexByte 是遠程名稱。

提供 .s 後綴的 stub 文件,以便編譯器繞過 -complete 的檢查,允許不完整的函數定義(譯註:指沒有函數體)。

例子 goparkunlock

package mainimport ( _ "unsafe" "fmt" "runtime/pprof" "os" "time")// Event types in the trace, args are given in square brackets.const ( traceEvGoBlock = 20 // goroutine blocks [timestamp, stack])type mutex struct { // Futex-based impl treats it as uint32 key, // while sema-based impl as M* waitm. // Used to be a union, but unions break precise GC. key uintptr}//go:linkname lock runtime.lockfunc lock(l *mutex)//go:linkname unlock runtime.unlockfunc unlock(l *mutex)//go:linkname goparkunlock runtime.goparkunlockfunc goparkunlock(lock *mutex, reason string, traceEv byte, traceskip int)func main() { l := &mutex{} go func() { lock(l) goparkunlock(l, "xxx", traceEvGoBlock, 1) }() for { pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) time.Sleep(time.Second * 1) }}

源碼

可在這裡獲取 https://github.com/sitano/gsysint 。

相關文章

Docker Windows install instructions on the state of 4 August 2016 04 Aug 2016PowerShell ducklish typed 25 Apr 2016Approach into strong typed configuration management DSL with FAKE, F#, WinRM and PowerShell 15 Mar 2016via: https://sitano.github.io/2016/04/28/golang-private/

作者:JohnKoepi 譯者:kekemuyu 校對:polaris1119

本文由 GCTT 原創編譯,Go語言中文網 榮譽推出

本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻麼?歡迎加入 GCTT!

翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯繫我們。

歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在正文中標註並保留原文/譯文連結和作者/譯者等信息。

文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽

相關焦點

  • 「GCTT 出品」Go 語言中的 Monkey 補丁
    很多人認為 monkey 補丁只能在動態語言,比如 Ruby 和 Python 中才存在。但是,這並不對。因為計算機只是很笨的機器,我們總能讓它做我們想讓它做的事兒!讓我們看看 Go 中的函數是怎麼工作的,並且,我們如何在運行時修改它們。本文會用到大量的 Intel 彙編,所以,我假設你可以讀彙編代碼,或者在讀本文時正拿著參考手冊.
  • 「GCTT 出品」Go 執行追蹤器(execution tracer)
    阻塞/解鎖goroutine的一些事件(系統調用,channel,鎖)網絡I/O相關事件系統調用垃圾回收追蹤器會原原本本地收集這些信息,不做任何聚合或者抽樣操作。對於負載高的應用來說,就可能會生成一個比較大的文件,該文件後面可以通過 go tool trace 命令來進行解析。
  • 『GCTT出品』通過 go/parser 理解 Go
    justforfunc 前情提要我們在上一篇文章中使用 go/scanner 找出了標準庫中最常用的標識符。這個標識符就是 v為了能獲取到更有價值的信息,我們只考慮大於等於三個字符的標識符。不出所料,在 Go 中最具代表性的判斷語句 if err != nil {} 中的 err 和 nil 出現的最為頻繁。
  • 「GCTT 出品」關於結構化並發的筆記——Go 語言中有害的聲明語句
    如果你有一個函數,並且在函數內部有一個循環,並且在循環內部有一個if/else,並且在 if/else 中有一個 goto ...那麼 goto 可以將控制發送到任何它想要的地方。也許控制會突然從另一個你還沒有調用的函數完全返回,你不知道!這就打破了抽象:這意味著每一個函數調用都可能是一個變相的 goto 語句,唯一需要知道的就是將系統的整個原始碼一次性保存在頭腦中。
  • 『GCTT出品』接受 interface 參數,返回 struct 在 go 中意味著什麼
    Go 語言從結構中抽象出接口,這種處理方式會產生嵌入複雜性。遵循你並不需要它軟體設計理念,如果不需要就沒有理由增加複雜性。一個常見的返回接口的理由是讓用戶把注意力放在函數所提供的 API 上。在 Go 中因為隱含實現了接口,所以這並不需要。返回結構的公共函數就成為那個API。
  • 『GCTT 出品』Go 系列教程 —— 35. 讀取文件
    文件讀取是所有程式語言中最常見的操作之一。本教程我們會學習如何使用 Go 讀取文件。本教程分為如下小節。將整個文件讀取到內存使用絕對文件路徑使用命令行標記來傳遞文件路徑將文件綁定在二進位文件中分塊讀取文件逐行讀取文件將整個文件讀取到內存將整個文件讀取到內存是最基本的文件操作之一。這需要使用 ioutil 包中的 ReadFile 函數。
  • 『GCTT 出品』Go 中 defer 的 5 個坑 - 第一部分
    名為 func 的函數一直運行至結束,然後 defer 函數會被執行且會因為值為 nil 而產生 panic 異常。然而值得注意的是,run() 的聲明是沒有問題,因為在外圍函數運行完成後它才會被調用。上面只是一個簡單的案例,但同樣的案例也可能發生在真實世界中,所以如果你遇上的話,可以想想是不是掉進了這個坑裡。
  • 「Go 語言教程」Go語言函數說明
    我們知道在程序設計中,模塊設計是最小的設計單元,模塊設計可以對應面相對象設計中的類設計,也可以對應到函數(方法)設計。編程中很多時候函數作為模塊設計的最小單元。函數設計也有很多方法和規定,以及設計原則。那麼go語言的函數都是怎麼樣子的,都有些什麼原則和要求呢,那麼怎麼做好函數方法設計呢,就讓我們一起來學習學習。
  • 「GCTT 出品」與 Jupyter 交互的 Go 編程
    在機器學習中首選 Python 有很多原因,其中一個原因是 Python 是為交互式代碼編寫和計算而設計的。另一個重要的原因是 Python 中有一個很好的交互式編程工具:Jupyter Notebook。雖然我現在在許多以前使用 Python 的項目中使用 Go 語言,但我仍然需要使用 Python 進行機器學習研究和數據分析。
  • 『GCTT出品』Go 函數 -- Go 語言新手的帶圖教程
    首先:聲明一個 Len 函數funcLen(s string)int {return utf8.RuneCountInString(s)} 然後:通過它的名字調用它Len("Hello world ")在線運行程序輸入參數和返回值類型輸入參數被用來把數據傳遞給函數。返回值類型被用來從函數中返回數據。
  • 『GCTT 出品』Golang 中 defer 的五個坑 - 第二部分
    例子func main() {for i :=; i < 4; i++ {defer fmt.Print(i) }}輸出Go 的運行時會將延遲執行的函數保存至一個棧中(譯註:意味著它們會按照入棧的順序倒序執行)。想了解更多,請閱讀這篇 文章 GCTT 出品的譯文:圖解 Go 中的延遲調用 defer。
  • 在 Go 語言中,我為什麼使用接口
    接口的簡單介紹在任一程式語言中,接口——方法或行為的集合,在功能和該功能的使用者之間構建了一層薄薄的抽象層。在使用接口時,並不需要了解底層函數是如何實現的,因為接口隔離了各個部分(劃重點)。跟不使用接口相比,使用接口的最大好處就是可以使代碼變得簡潔。
  • Python基礎——標識符的使用
    Python基礎——標識符的使用標識符是電腦語言中允許作為名字的有效字符串集合。其中,有一部分是關鍵字,構成語言的標識符。這樣的標識符是不能做它用的標識符的,否則會引起語法錯誤(SyntaxError 異常)。
  • 手把手教會你帶你理解Go語言中的包
    包的注意事項如果這個文件夾要當包使用文件夾名中不能包含_。導入包上面我們知道了包是如何定義的。並且在和main.go同級的項目目錄下建了一個clac包。在clac包下有倆個文件一個是add.go一個是sub.go兩個文件夾分別都有對應的方法。
  • 「Go系列」 理解GO Routine 和 Channel
    普通函數和go routine 之間的主要區別在於,主程序不等待GO例程完成,這在正常功能的情況下是不正確的。Go routine 是一個非阻塞函數,一旦調用,它便會自行執行。Go routine 可以被理解為輕量級線程。創建和運行Go routine 的成本很低,因此在GO應用程式中可以同時運行數千個Go routine。每個Go routine只有幾kb堆棧大小。
  • go-zero 是如何追蹤你的請求鏈路的
    go-zero 是如何追蹤你的請求鏈路微服務架構中,調用鏈可能很漫長,從 http 到 rpc ,又從 rpc 到 http 。代碼結構:保存鏈路的上下文信息「traceid,spanid,或者是其他想要傳遞的內容」:鏈路中的一個操作,存儲時間和某些信息: trace 傳播下遊的操作「抽取,注入」:實現了空的 tracer 實現
  • 如何定義一個和庫函數名一樣的函數,並在函數中調用該庫函數
    read()我們現在要自己定義一個名字一樣的函數read(),main()函數首先調用我們自己定義的函數read()自己定義的函數,要再定義庫文件中的read()函數。read系統調用read現在我們想定義一個自己的函數,名字也是read,要如何操作呢?
  • 在PHP中如何為匿名函數指定this?
    在之前的文章中,我們已經學習過匿名函數的使用,沒有看過的小夥伴可以進入傳送門先去了解下閉包匿名函數的用法,傳送:還不知道PHP有閉包?那你真OUT了。關於閉包匿名函數,在JS中有個很典型的問題就是要給它綁定一個 this 作用域。
  • Python中如何創建和調用函數
    第八十五節:創建和調用函數一直以來,數學函數是我輩最大的緊箍咒,現在遇到Python中的函數,就這區區兩個字,竟然一度讓我有了退卻的念頭,鼓起勇氣學了一點點,感覺沒有那麼難,嗯,可以繼續,下面就把今天學習的一點心得分享給大家。不提數學函數了,直接說說Python中的函數的用途。
  • Dubbo-go 源碼筆記(二)客戶端調用過程
    、註冊協議、接口 id、調用方法、集群策略等,這些配置都會在之後與註冊組件交互、重寫 ivk、調用的過程中使用到。在 main 函數中,同樣調用了 config.Load() 函數,之後就可以通過實現好的 rpc-service:userProvider 直接調用對應的功能函數,即可實現 rpc 調用。