也談Go的可移植性

LynellP48 7年前發布 | 27K 次閱讀 Cgo pthread Google Go/Golang開發

Go有很多優點,比如:簡單、原生支持并發等,而不錯的 可移植性 也是Go被廣大程序員接納的重要因素之一。但你知道為什么Go語言擁有很好的平臺可移植性嗎?本著“知其然,亦要知其所以然”的精神,本文我們就來探究一下Go良好可移植性背后的原理。

一、Go的可移植性

說到一門編程語言可移植性,我們一般從下面兩個方面考量:

  • 語言自身被移植到不同平臺的容易程度;
  • 通過這種語言編譯出來的應用程序對平臺的適應性。

在Go 1.7及以后版本中,我們可以通過下面命令查看Go支持OS和平臺列表:

$go tool dist list
android/386
android/amd64
android/arm
android/arm64
darwin/386
darwin/amd64
darwin/arm
darwin/arm64
dragonfly/amd64
freebsd/386
freebsd/amd64
freebsd/arm
linux/386
linux/amd64
linux/arm
linux/arm64
linux/mips
linux/mips64
linux/mips64le
linux/mipsle
linux/ppc64
linux/ppc64le
linux/s390x
nacl/386
nacl/amd64p32
nacl/arm
netbsd/386
netbsd/amd64
netbsd/arm
openbsd/386
openbsd/amd64
openbsd/arm
plan9/386
plan9/amd64
plan9/arm
solaris/amd64
windows/386
windows/amd64

從上述列表我們可以看出:從 linux/arm64 的嵌入式系統到 linux/s390x 的大型機系統,再到Windows、linux和darwin(mac)這樣的主流操作系統、amd64、386這樣的主流處理器體系,Go對各種平臺和操作系統的支持不可謂不廣泛。

Go官方似乎沒有給出明確的porting guide,關于將Go語言porting到其他平臺上的內容更多是在 golang-dev 這樣的小圈子中討論的事情。但就Go語言這么短的時間就能很好的支持這么多平臺來看,Go的porting還是相對easy的。從個人對Go的了解來看,這一定程度上得益于Go獨立實現了runtime。

runtime是支撐程序運行的基礎。我們最熟悉的莫過于libc(C運行時),它是目前主流操作系統上應用最普遍的運行時,通常以動態鏈接庫的形式(比如:/lib/x86_64-linux-gnu/libc.so.6)隨著系統一并發布,它的功能大致有如下幾個:

  • 提供基礎庫函數調用,比如:strncpy;
  • 封裝syscall(注:syscall是操作系統提供的API口,當用戶層進行系統調用時,代碼會trap(陷入)到內核層面執行),并提供同語言的庫函數調用,比如:malloc、fread等;
  • 提供程序啟動入口函數,比如:linux下的__libc_start_main。

libc等c runtime lib是很早以前就已經實現的了,甚至有些老舊的libc還是單線程的。一些從事c/c++開發多年的程序員早年估計都有過這樣的經歷:那就是鏈接runtime庫時甚至需要選擇鏈接支持多線程的庫還是只支持單線程的庫。除此之外,c runtime的版本也參差不齊。這樣的c runtime狀況完全不能滿足go語言自身的需求;另外Go的目標之一是原生支持并發,并使用goroutine模型,c runtime對此是無能為力的,因為c runtime本身是基于線程模型的。綜合以上因素,Go自己實現了runtime,并封裝了syscall,為不同平臺上的go user level代碼提供封裝完成的、統一的go標準庫;同時Go runtime實現了對goroutine模型的支持。

獨立實現的go runtime層將Go user-level code與OS syscall解耦,把Go porting到一個新平臺時,將runtime與新平臺的syscall對接即可(當然porting工作不僅僅只有這些);同時,runtime層的實現基本擺脫了Go程序對libc的依賴,這樣靜態編譯的Go程序具有很好的平臺適應性。比如:一個compiled for linux amd64的Go程序可以很好的運行于不同linux發行版(centos、ubuntu)下。

以下測試試驗環境為:darwin amd64Go 1.8。

二、默認”靜態鏈接”的Go程序

我們先來寫兩個程序:hello.c和hello.go,它們完成的功能都差不多,在stdout上輸出一行文字:

//hello.c
#include <stdio.h>

int main() {
        printf("%s\n", "hello, portable c!");
        return 0;
}

//hello.go
package main

import "fmt"

func main() {
    fmt.Println("hello, portable go!")
}

我們采用“默認”方式分別編譯以下兩個程序:

$cc -o helloc hello.c
$go build -o hellogo hello.go

$ls -l
-rwxr-xr-x    1 tony  staff     8496  6 27 14:18 helloc*
-rwxr-xr-x    1 tony  staff  1628192  6 27 14:18 hellogo*

從編譯后的兩個文件helloc和hellogo的size上我們可以看到hellogo相比于helloc簡直就是“巨人”般的存在,其size近helloc的200倍。略微學過一些Go的人都知道,這是因為hellogo中包含了必需的go runtime。我們通過otool工具(linux上可以用ldd)查看一下兩個文件的對外部動態庫的依賴情況:

$otool -L helloc
helloc:
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1197.1.1)
$otool -L hellogo
hellogo:

通過otool輸出,我們可以看到hellogo并不依賴任何外部庫,我們將hellog這個二進制文件copy到任何一個mac amd64的平臺上,均可以運行起來。而helloc則依賴外部的動態庫:/usr/lib/libSystem.B.dylib,而libSystem.B.dylib這個動態庫還有其他依賴。我們通過nm工具可以查看到helloc具體是哪個函數符號需要由外部動態庫提供:

$nm helloc
0000000100000000 T __mh_execute_header
0000000100000f30 T _main
                 U _printf
                 U dyld_stub_binder

可以看到:_printf和dyld_stub_binder兩個符號是未定義的(對應的前綴符號是U)。如果對hellog使用nm,你會看到大量符號輸出,但沒有未定義的符號。

$nm hellogo
00000000010bb278 s $f64.3eb0000000000000
00000000010bb280 s $f64.3fd0000000000000
00000000010bb288 s $f64.3fe0000000000000
00000000010bb290 s $f64.3fee666666666666
00000000010bb298 s $f64.3ff0000000000000
00000000010bb2a0 s $f64.4014000000000000
00000000010bb2a8 s $f64.4024000000000000
00000000010bb2b0 s $f64.403a000000000000
00000000010bb2b8 s $f64.4059000000000000
00000000010bb2c0 s $f64.43e0000000000000
00000000010bb2c8 s $f64.8000000000000000
00000000010bb2d0 s $f64.bfe62e42fefa39ef
000000000110af40 b __cgo_init
000000000110af48 b __cgo_notify_runtime_init_done
000000000110af50 b __cgo_thread_start
000000000104d1e0 t __rt0_amd64_darwin
000000000104a0f0 t _callRet
000000000104b580 t _gosave
000000000104d200 T _main
00000000010bbb20 s _masks
000000000104d370 t _nanotime
000000000104b7a0 t _setg_gcc
00000000010bbc20 s _shifts
0000000001051840 t errors.(*errorString).Error
00000000010517a0 t errors.New
.... ...
0000000001065160 t type..hash.time.Time
0000000001064f70 t type..hash.time.zone
00000000010650a0 t type..hash.time.zoneTrans
0000000001051860 t unicode/utf8.DecodeRuneInString
0000000001051a80 t unicode/utf8.EncodeRune
0000000001051bd0 t unicode/utf8.RuneCount
0000000001051d10 t unicode/utf8.RuneCountInString
0000000001107080 s unicode/utf8.acceptRanges
00000000011079e0 s unicode/utf8.first

$nm hellogo|grep " U "

Go將所有運行需要的函數代碼都放到了hellogo中,這就是所謂的“靜態鏈接”。是不是所有情況下,Go都不會依賴外部動態共享庫呢?我們來看看下面這段代碼:

//server.go
package main

import (
    "log"
    "net/http"
    "os"
)

func main() {
    cwd, err := os.Getwd()
    if err != nil {
        log.Fatal(err)
    }

    srv := &http.Server{
        Addr:    ":8000", // Normally ":443"
        Handler: http.FileServer(http.Dir(cwd)),
    }
    log.Fatal(srv.ListenAndServe())
}

我們利用Go標準庫的net/http包寫了一個fileserver,我們build一下該server,并查看它是否有外部依賴以及未定義的符號:

$go build server.go
-rwxr-xr-x    1 tony  staff  5943828  6 27 14:47 server*

$otool -L server
server:
    /usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0)
    /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 0.0.0, current version 0.0.0)
    /System/Library/Frameworks/Security.framework/Versions/A/Security (compatibility version 0.0.0, current version 0.0.0)
    /usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0)
    /usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0)

$nm server |grep " U "
                 U _CFArrayGetCount
                 U _CFArrayGetValueAtIndex
                 U _CFDataAppendBytes
                 U _CFDataCreateMutable
                 U _CFDataGetBytePtr
                 U _CFDataGetLength
                 U _CFDictionaryGetValueIfPresent
                 U _CFEqual
                 U _CFNumberGetValue
                 U _CFRelease
                 U _CFStringCreateWithCString
                 U _SecCertificateCopyNormalizedIssuerContent
                 U _SecCertificateCopyNormalizedSubjectContent
                 U _SecKeychainItemExport
                 U _SecTrustCopyAnchorCertificates
                 U _SecTrustSettingsCopyCertificates
                 U _SecTrustSettingsCopyTrustSettings
                 U ___error
                 U ___stack_chk_fail
                 U ___stack_chk_guard
                 U ___stderrp
                 U _abort
                 U _fprintf
                 U _fputc
                 U _free
                 U _freeaddrinfo
                 U _fwrite
                 U _gai_strerror
                 U _getaddrinfo
                 U _getnameinfo
                 U _kCFAllocatorDefault
                 U _malloc
                 U _memcmp
                 U _nanosleep
                 U _pthread_attr_destroy
                 U _pthread_attr_getstacksize
                 U _pthread_attr_init
                 U _pthread_cond_broadcast
                 U _pthread_cond_wait
                 U _pthread_create
                 U _pthread_key_create
                 U _pthread_key_delete
                 U _pthread_mutex_lock
                 U _pthread_mutex_unlock
                 U _pthread_setspecific
                 U _pthread_sigmask
                 U _setenv
                 U _strerror
                 U _sysctlbyname
                 U _unsetenv

通過otool和nm的輸出結果我們驚訝的看到:默認采用“靜態鏈接”的Go程序怎么也要依賴外部的動態鏈接庫,并且也包含了許多“未定義”的符號了呢?問題在于cgo。

三、cgo對可移植性的影響

默認情況下,Go的runtime環境變量CGO_ENABLED=1,即默認開始cgo,允許你在Go代碼中調用C代碼,Go的pre-compiled標準庫的.a文件也是在這種情況下編譯出來的。在$GOROOT/pkg/darwin_amd64中,我們遍歷所有預編譯好的標準庫.a文件,并用nm輸出每個.a的未定義符號,我們看到下面一些包是對外部有依賴的(動態鏈接):

=> crypto/x509.a
                 U _CFArrayGetCount
                 U _CFArrayGetValueAtIndex
                 U _CFDataAppendBytes
                 ... ...
                 U _SecCertificateCopyNormalizedIssuerContent
                 U _SecCertificateCopyNormalizedSubjectContent
                 ... ...
                 U ___stack_chk_fail
                 U ___stack_chk_guard
                 U __cgo_topofstack
                 U _kCFAllocatorDefault
                 U _memcmp
                 U _sysctlbyname

=> net.a
                 U ___error
                 U __cgo_topofstack
                 U _free
                 U _freeaddrinfo
                 U _gai_strerror
                 U _getaddrinfo
                 U _getnameinfo
                 U _malloc

=> os/user.a
                 U __cgo_topofstack
                 U _free
                 U _getgrgid_r
                 U _getgrnam_r
                 U _getgrouplist
                 U _getpwnam_r
                 U _getpwuid_r
                 U _malloc
                 U _realloc
                 U _sysconf

=> plugin.a
                 U __cgo_topofstack
                 U _dlerror
                 U _dlopen
                 U _dlsym
                 U _free
                 U _malloc
                 U _realpath$DARWIN_EXTSN

=> runtime/cgo.a
                 ... ...
                 U _abort
                 U _fprintf
                 U _fputc
                 U _free
                 U _fwrite
                 U _malloc
                 U _nanosleep
                 U _pthread_attr_destroy
                 U _pthread_attr_getstacksize
                 ... ...
                 U _setenv
                 U _strerror
                 U _unsetenv

=> runtime/race.a
                 U _OSSpinLockLock
                 U _OSSpinLockUnlock
                 U __NSGetArgv
                 U __NSGetEnviron
                 U __NSGetExecutablePath
                 U ___error
                 U ___fork
                 U ___mmap
                 U ___munmap
                 U ___stack_chk_fail
                 U ___stack_chk_guard
                 U __dyld_get_image_header
                .... ...

我們以os/user為例,在CGO_ENABLED=1,即cgo開啟的情況下,os/user包中的lookupUserxxx系列函數采用了c版本的實現,我們看到在$GOROOT/src/os/user/lookup_unix.go中的build tag中包含了 +build cgo 。這樣一來,在CGO_ENABLED=1,該文件將被編譯,該文件中的c版本實現的lookupUser將被使用:

// +build darwin dragonfly freebsd !android,linux netbsd openbsd solaris
// +build cgo

package user
... ...
func lookupUser(username string) (*User, error) {
    var pwd C.struct_passwd
    var result *C.struct_passwd
    nameC := C.CString(username)
    defer C.free(unsafe.Pointer(nameC))
    ... ...
}

這樣來看,凡是依賴上述包的Go代碼最終編譯的可執行文件都是要有外部依賴的。不過我們依然可以通過disable CGO_ENABLED來編譯出純靜態的Go程序:

$CGO_ENABLED=0 go build -o server_cgo_disabled server.go

$otool -L server_cgo_disabled
server_cgo_disabled:
$nm server_cgo_disabled |grep " U "

如果你使用build的 “-x -v”選項,你將看到go compiler會重新編譯依賴的包的靜態版本,包括net、mime/multipart、crypto/tls等,并將編譯后的.a(以包為單位)放入臨時編譯器工作目錄($WORK)下,然后再靜態連接這些版本。

四、internal linking和external linking

問題來了:在CGO_ENABLED=1這個默認值的情況下,是否可以實現純靜態連接呢?答案是可以。在$GOROOT/cmd/cgo/doc.go中,文檔介紹了cmd/link的兩種工作模式:internal linking和external linking。

1、internal linking

internal linking的大致意思是若用戶代碼中僅僅使用了net、os/user等幾個標準庫中的依賴cgo的包時,cmd/link默認使用internal linking,而無需啟動外部external linker(如:gcc、clang等),不過由于cmd/link功能有限,僅僅是將.o和pre-compiled的標準庫的.a寫到最終二進制文件中。因此如果標準庫中是在CGO_ENABLED=1情況下編譯的,那么編譯出來的最終二進制文件依舊是動態鏈接的,即便在go build時傳入-ldflags ‘extldflags “-static”‘亦無用,因為根本沒有使用external linker:

$go build -o server-fake-static-link  -ldflags '-extldflags "-static"' server.go
$otool -L server-fake-static-link
server-fake-static-link:
    /usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0)
    /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 0.0.0, current version 0.0.0)
    /System/Library/Frameworks/Security.framework/Versions/A/Security (compatibility version 0.0.0, current version 0.0.0)
    /usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0)
    /usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0)

2、external linking

而external linking機制則是cmd/link將所有生成的.o都打到一個.o文件中,再將其交給外部的鏈接器,比如gcc或clang去做最終鏈接處理。如果此時,我們在cmd/link的參數中傳入-ldflags ‘extldflags “-static”‘,那么gcc/clang將會去做靜態鏈接,將.o中undefined的符號都替換為真正的代碼。我們可以通過-linkmode=external來強制cmd/link采用external linker,還是以server.go的編譯為例:

$go build -o server-static-link  -ldflags '-linkmode "external" -extldflags "-static"' server.go
# command-line-arguments
/Users/tony/.bin/go18/pkg/tool/darwin_amd64/link: running clang failed: exit status 1
ld: library not found for -lcrt0.o
clang: error: linker command failed with exit code 1 (use -v to see invocation)

可以看到,cmd/link調用的clang嘗試去靜態連接libc的.a文件,但由于我的mac上僅僅有libc的dylib,而沒有.a,因此靜態連接失敗。我找到一個ubuntu 16.04環境:重新執行上述構建命令:

# go build -o server-static-link  -ldflags '-linkmode "external" -extldflags "-static"' server.go
# ldd server-static-link
    not a dynamic executable
# nm server-static-link|grep " U "

該環境下libc.a和libpthread.a分別在下面兩個位置:

/usr/lib/x86_64-linux-gnu/libc.a
/usr/lib/x86_64-linux-gnu/libpthread.a

就這樣,我們在CGO_ENABLED=1的情況下,也編譯構建出了一個純靜態鏈接的Go程序。

如果你的代碼中使用了C代碼,并依賴cgo在go中調用這些c代碼,那么cmd/link將會自動選擇external linking的機制:

//testcgo.go
package main

//#include <stdio.h>
// void foo(char *s) {
//    printf("%s\n", s);
// }
// void bar(void *p) {
//    int *q = (int*)p;
//    printf("%d\n", *q);
// }
import "C"
import (
    "fmt"
    "unsafe"
)

func main() {
    var s = "hello"
    C.foo(C.CString(s))

    var i int = 5
    C.bar(unsafe.Pointer(&i))

    var i32 int32 = 7
    var p *uint32 = (*uint32)(unsafe.Pointer(&i32))
    fmt.Println(*p)
}

編譯testcgo.go:

# go build -o testcgo-static-link  -ldflags '-extldflags "-static"' testcgo.go
# ldd testcgo-static-link
    not a dynamic executable

vs.
# go build -o testcgo testcgo.go
# ldd ./testcgo
    linux-vdso.so.1 =>  (0x00007ffe7fb8d000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fc361000000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc360c36000)
    /lib64/ld-linux-x86-64.so.2 (0x000055bd26d4d000)

五、小結

本文探討了Go的可移植性以及哪些因素對Go編譯出的程序的移植性有影響:

  • 你的程序用了哪些標準庫包?如果僅僅是非net、os/user等的普通包,那么你的程序默認將是純靜態的,不依賴任何c lib等外部動態鏈接庫;
  • 如果使用了net這樣的包含cgo代碼的標準庫包,那么CGO_ENABLED的值將影響你的程序編譯后的屬性:是靜態的還是動態鏈接的;
  • CGO_ENABLED=0的情況下,Go采用純靜態編譯;
  • 如果CGO_ENABLED=1,但依然要強制靜態編譯,需傳遞-linkmode=external給cmd/link。

 

 

來自:http://tonybai.com/2017/06/27/an-intro-about-go-portability/

 

 本文由用戶 LynellP48 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!