Lý do cho kích thước khổng lồ của tệp thực thi được biên dịch của Go


90

Tôi đã tuân thủ một chương trình hello world Go tạo ra tệp thực thi gốc trên máy linux của tôi. Nhưng tôi đã rất ngạc nhiên khi thấy dung lượng của chương trình Hello world Go đơn giản, nó là 1,9MB!

Tại sao tệp thực thi của một chương trình đơn giản như vậy trong Go lại rất lớn?


22
Khổng lồ? Tôi đoán bạn không làm nhiều Java sau đó!
Rick-777

19
Vâng, tôi đến từ nền tảng C / C ++!
Karthic Rao

Tôi vừa thử thế giới hello world theo kiểu scala-native này: scala-native.org/en/latest/user/sbt.html#minimal-sbt-project Phải mất khá nhiều thời gian để biên dịch, tải xuống rất nhiều thứ và nhị phân là 3,9 MB.
Bli

Tôi đã cập nhật câu trả lời của mình bên dưới với những phát hiện năm 2019.
VonC

1
Ứng dụng Hello World đơn giản trong C # .NET Core 3.1 với việc dotnet publish -r win-x64 -p:publishsinglefile=true -p:publishreadytorun=true -p:publishtrimmed=truetạo một tệp nhị phân khoảng ~ 26MB!
Jalal

Câu trả lời:


90

Câu hỏi chính xác này xuất hiện trong Câu hỏi thường gặp chính thức: Tại sao chương trình tầm thường của tôi lại là một tệp nhị phân lớn như vậy?

Trích dẫn câu trả lời:

Các linkers trong chuỗi công cụ gc ( 5l, 6l8l) làm liên kết tĩnh. Do đó, tất cả các mã nhị phân của Go đều bao gồm thời gian chạy của Go, cùng với thông tin loại thời gian chạy cần thiết để hỗ trợ kiểm tra kiểu động, phản chiếu và thậm chí cả dấu vết ngăn xếp thời gian hoảng loạn.

Một chương trình C "hello, world" đơn giản được biên dịch và liên kết tĩnh bằng cách sử dụng gcc trên Linux có dung lượng khoảng 750 kB, bao gồm cả việc triển khai printf. Một chương trình fmt.Printfcờ vây tương đương sử dụng có dung lượng khoảng 1,9 MB, nhưng bao gồm thông tin loại và hỗ trợ thời gian chạy mạnh mẽ hơn.

Vì vậy, tệp thực thi gốc của Hello World của bạn là 1,9 MB vì ​​nó chứa thời gian chạy cung cấp tính năng thu thập rác, phản chiếu và nhiều tính năng khác (mà chương trình của bạn có thể không thực sự sử dụng, nhưng nó ở đó). Và việc triển khai fmtgói mà bạn đã sử dụng để in "Hello World"văn bản (cộng với các phụ thuộc của nó).

Bây giờ, hãy thử cách sau: thêm một fmt.Println("Hello World! Again")dòng khác vào chương trình của bạn và biên dịch lại. Kết quả sẽ không phải là 2x 1,9MB, mà chỉ là 1,9 MB! Có, bởi vì tất cả các thư viện được sử dụng ( fmtvà các phụ thuộc của nó) và thời gian chạy đã được thêm vào tệp thực thi (và do đó, chỉ một vài byte nữa sẽ được thêm vào để in văn bản thứ 2 mà bạn vừa thêm vào).


10
Chương trình AC "hello world", liên kết tĩnh với glibc là 750K vì glibc rõ ràng không được thiết kế cho liên kết tĩnh và thậm chí không thể liên kết tĩnh đúng cách trong một số trường hợp. Một chương trình "hello world" được liên kết tĩnh với musl libc là 14K.
Craig Barnes

Tôi vẫn đang tìm kiếm, tuy nhiên, sẽ rất tốt nếu biết những gì được liên kết trong đó để có thể kẻ tấn công không liên kết bằng mã độc.
Richard

Vậy tại sao thư viện thời gian chạy Go không có trong tệp DLL, để nó có thể được chia sẻ giữa tất cả các tệp exe của Go? Sau đó, một chương trình "hello world" có thể là một vài KB, như mong đợi, thay vì 2 MB. Có toàn bộ thư viện thời gian chạy trong mọi chương trình là một lỗ hổng nghiêm trọng đối với giải pháp thay thế tuyệt vời cho MSVC trên Windows.
David Spector

Tốt hơn là tôi nên lường trước sự phản đối cho nhận xét của mình: rằng cờ vây "được liên kết tĩnh". Được rồi, không có DLL nào nữa. Nhưng liên kết tĩnh không có nghĩa là bạn cần liên kết trong (ràng buộc) toàn bộ thư viện, chỉ các hàm thực sự được sử dụng trong thư viện!
David Spector

43

Hãy xem xét chương trình sau:

package main

import "fmt"

func main() {
    fmt.Println("Hello World!")
}

Nếu tôi tạo điều này trên máy Linux AMD64 (Go 1.9) của mình, như sau:

$ go build
$ ls -la helloworld
-rwxr-xr-x 1 janf group 2029206 Sep 11 16:58 helloworld

Tôi nhận được aa nhị phân có kích thước khoảng 2 Mb.

Lý do cho điều này (đã được giải thích trong các câu trả lời khác) là chúng ta đang sử dụng gói "fmt" khá lớn, nhưng nhị phân cũng chưa bị loại bỏ và điều này có nghĩa là bảng ký hiệu vẫn còn đó. Thay vào đó, nếu chúng tôi hướng dẫn trình biên dịch tách tệp nhị phân, nó sẽ trở nên nhỏ hơn nhiều:

$ go build -ldflags "-s -w"
$ ls -la helloworld
-rwxr-xr-x 1 janf group 1323616 Sep 11 17:01 helloworld

Tuy nhiên, nếu chúng ta viết lại chương trình để sử dụng hàm print, thay vì fmt.Println, như sau:

package main

func main() {
    print("Hello World!\n")
}

Và sau đó biên dịch nó:

$ go build -ldflags "-s -w"
$ ls -la helloworld
-rwxr-xr-x 1 janf group 714176 Sep 11 17:06 helloworld

Chúng tôi kết thúc với một nhị phân thậm chí còn nhỏ hơn. Điều này nhỏ nhất mà chúng ta có thể lấy được mà không cần dùng đến các thủ thuật như đóng gói UPX, vì vậy chi phí của thời gian chạy Go là khoảng 700 Kb.


3
UPX nén các tệp nhị phân và giải nén chúng nhanh chóng khi chúng được thực thi. Tôi sẽ không coi nó là một thủ thuật mà không giải thích nó làm gì, vì nó có thể hữu ích trong một số trường hợp. Kích thước nhị phân được giảm bớt phần nào do thời gian khởi động và sử dụng RAM; hơn nữa, hiệu suất cũng có thể bị ảnh hưởng một chút. Như một ví dụ, một tệp thực thi có thể được thu nhỏ xuống 30% kích thước (bị loại bỏ) và mất 35 mili giây để chạy.
simlev

10

Lưu ý rằng vấn đề kích thước nhị phân được theo dõi bởi vấn đề 6853 trong dự án golang / go .

Ví dụ: commit a26c01a (cho Go 1.4) cắt hello world đi 70kB :

vì chúng ta không viết những tên đó vào bảng ký hiệu.

Xem xét trình biên dịch, trình hợp dịch, trình liên kết và thời gian chạy cho 1.5 sẽ hoàn toàn ở trong Go, bạn có thể mong đợi tối ưu hóa hơn nữa.


Cập nhật 2016 Go 1.7: điều này đã được tối ưu hóa: xem " Các tệp nhị phân Go 1.7 nhỏ hơn ".

Nhưng ngày này (tháng 4 năm 2019), điều gì diễn ra nhiều nhất runtime.pclntab.
Xem " Tại sao các tệp thực thi Go của tôi quá lớn? Hình ảnh hóa kích thước của tệp thực thi Go bằng D3 " từ Raphael 'kena' Poss .

Nó không được ghi chép quá đầy đủ tuy nhiên nhận xét này từ mã nguồn Go cho thấy mục đích của nó:

// A LineTable is a data structure mapping program counters to line numbers.

Mục đích của cấu trúc dữ liệu này là cho phép hệ thống thời gian chạy Go tạo dấu vết ngăn xếp mô tả khi gặp sự cố hoặc theo yêu cầu nội bộ thông qua runtime.GetStackAPI.

Vì vậy, nó có vẻ hữu ích. Nhưng tại sao nó lại lớn như vậy?

URL https://golang.org/s/go12symtab ẩn trong tệp nguồn được liên kết ở trên chuyển hướng đến tài liệu giải thích điều gì đã xảy ra giữa Go 1.0 và 1.2. Để diễn dải:

trước 1.2, trình liên kết Go đã tạo ra một bảng dòng được nén và chương trình sẽ giải nén nó khi khởi tạo tại thời điểm chạy.

trong Go 1.2, một quyết định đã được đưa ra để mở rộng trước bảng dòng trong tệp thực thi thành định dạng cuối cùng phù hợp để sử dụng trực tiếp tại thời điểm chạy mà không cần thêm bước giải nén.

Nói cách khác, nhóm Go đã quyết định làm cho các tệp thực thi lớn hơn để tiết kiệm thời gian khởi tạo.

Ngoài ra, nhìn vào cấu trúc dữ liệu, có vẻ như kích thước tổng thể của nó trong các tệp nhị phân đã biên dịch là siêu tuyến tính về số lượng các hàm trong chương trình, ngoài độ lớn của mỗi hàm.

https://science.raphael.poss.name/go-executable-size-visualization-with-d3/size-demo-ss.png


2
Tôi không hiểu ngôn ngữ triển khai của anh ấy có liên quan gì đến nó. Họ cần sử dụng thư viện chia sẻ. Hơi khó tin khi họ chưa có trong thời đại ngày nay.
Marquis of Lorne

2
@EJP: Tại sao họ cần sử dụng thư viện chia sẻ?
Flimzy

9
@EJP, một phần của sự đơn giản của Go là không sử dụng các thư viện được chia sẻ. Trên thực tế, Go không có bất kỳ phụ thuộc nào cả, nó sử dụng các cuộc gọi tổng hợp đơn giản. Chỉ cần triển khai một nhị phân duy nhất và nó chỉ hoạt động. Nó sẽ làm tổn hại đáng kể đến ngôn ngữ và hệ sinh thái của nó nếu ngược lại.
creker

10
Một khía cạnh thường bị lãng quên của việc có các tệp nhị phân được liên kết tĩnh là có thể chạy chúng trong một Docker-container hoàn toàn trống. Từ quan điểm bảo mật, điều này là lý tưởng. Khi vùng chứa trống, bạn có thể đột nhập (nếu tệp nhị phân được liên kết tĩnh có lỗ hổng), nhưng vì không tìm thấy gì trong vùng chứa, cuộc tấn công dừng lại ở đó.
Joppe
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.