Vá GCC để build Actually Portable Executables
(ahgamut.github.io)- Để build được nhiều phần mềm C hiện có hơn với Cosmopolitan Libc, không chỉ cần qua mặt hệ thống build mà còn phải để compiler tự động biến đổi mã có vấn đề bằng một bản vá GCC khoảng 2.000 dòng
- Trở ngại là các giá trị hệ thống như
SIGTERM,EINVALkhông phải hằng số tại thời điểm biên dịch, nên vướng các quy tắc của C về nhãnswitch casevà khởi tạostatic/const struct - Đã thử
sed, regex Python, plugin GCC, thậm chí cả việc chặn macro tiền xử lý, nhưng lỗi nhãncasexảy ra trong lúc parsing nên khó chặn chỉ bằng plugin - Bản vá cuối cùng được bật bằng cờ
-fportcosmo, cho phép GCC biếnswitchthànhif/gotongay trước khi báo lỗi, đồng thời bổ sung phần điền giá trị runtime cho khởi tạo struct - Vẫn còn các giới hạn với struct ẩn danh,
enum, một sốconst int, và chỉ số mảng trong biểu thức khởi tạo, nhưng nó mở ra con đường build không cần sửa mã nguồn cho các phần mềm nhưbash,curl,git,ninja,gcc
Mục tiêu của bản vá GCC
- Một bản vá
gcckhoảng 2.000 dòng đã được viết để giúp tạo Actually Portable Executables dễ hơn với Cosmopolitan Libc - Sau khi áp dụng bản vá, có thể dùng hệ thống build
./configurehoặccmakeđể build các phần mềm saubashcurlgitninja- chính
gcc
- Các file thực thi được build ra phải có thể chạy trên Linux, FreeBSD, MacOS, OpenBSD, NetBSD và Windows
- Có lưu ý rằng việc chạy trên Windows chưa được kiểm thử trực tiếp
- Có thể tải các binary được build bằng GitHub Actions từ superconfigure releases
Điểm nghẽn khi port sang Cosmopolitan Libc
- Sau khi Cosmopolitan Libc thu hút chú ý vào năm 2021 nhờ webserver
redbean, câu hỏi quan trọng trở thành: phần mềm C hiện có hoạt động tốt đến đâu trên libc này - Trong quá trình port các runtime ngôn ngữ như Lua, Wren, Janet và
quickjs, cùng một mẫu vấn đề lặp đi lặp lại - Dùng các hằng hệ thống như
SIGTERM,EINVALtrong nhãncasecủa câu lệnhswitchsẽ xung đột với quy tắc biên dịch C - Chuẩn C yêu cầu nhãn
casephải là hằng số tại thời điểm biên dịch- Trong Cosmopolitan Libc, các macro tiền xử lý C liên quan đến giao diện hệ thống cần mang tính symbolic
- Kết quả là những đoạn mã phổ biến như
switch(errno) { case EINVAL: ... }có thể không biên dịch được
Biến switch thành if và goto
- Nếu các nhãn như
case EINVAL,case ENOSYSbên trongswitch(errno)không phải hằng số tại thời điểm biên dịch, GCC sẽ báo lỗicase label does not reduce to an integer constant - Bản vá tự động biến đổi câu lệnh
switchthành cấu trúc sau- Với mỗi giá trị
case, thêm một nhánh dạngif (errno == EINVAL) goto caselabel_EINVAL; - Thêm
gotođể chuyển tới nhãndefault - Khối
casegốc được dựng lại bằng nhãn vàgoto endofthis_switch
- Với mỗi giá trị
- Cách này không hẳn là thanh lịch nhất, nhưng có thể xử lý các trường hợp gặp trong nhiều codebase
- Vì còn phải xử lý cả các
switchtrộnifdefvà fallthrough, phép biến đổi dựa trêngotolà lựa chọn đơn giản
Khởi tạo struct cũng chịu cùng ràng buộc
- Vấn đề hằng số tại thời điểm biên dịch cũng xuất hiện trong khởi tạo
statichoặcconst struct - Nếu
EINVALkhông phải hằng số tại thời điểm biên dịch thì một số kiểu khởi tạo sau là không hợp lệ trong C- khởi tạo
structcục bộ thông thường bên trong hàm thì vẫn được const structstatic structconst structtoàn cụcstatic structtoàn cục
- khởi tạo
- Một ví dụ thực tế là module
faulthandlercủa CPython - Cách giải quyết là khởi tạo struct bằng giá trị giả, rồi chèn thêm mã điền giá trị đúng trước khi sử dụng
- Bên trong hàm thì thêm câu lệnh
if - Với khởi tạo toàn cục hoặc tĩnh thì dùng
__attribute__((constructor))
- Bên trong hàm thì thêm câu lệnh
Giới hạn của thay thế văn bản và plugin GCC
- Ban đầu, tác giả định tự động hóa phép biến đổi bằng shell script
sed, rồi sau đó dùng script Python và regex để xử lý cả fallthrough - Nhưng vì C preprocessor và
ifdef, việc thay thế văn bản khó có thể hoạt động hoàn chỉnh- Do rất khó biết trước tại thời điểm biên dịch
ifdefnào sẽ được kích hoạt
- Do rất khó biết trước tại thời điểm biên dịch
- Thử tiếp theo là dùng plugin GCC để thực hiện biến đổi AST
- Plugin GCC có thể được nạp bằng
-fplugin=your-plugin.so - Có thể truy cập AST ở các sự kiện như
PLUGIN_PRE_GENERICIZE,PLUGIN_FINISH_PARSE_FUNCTION,PLUGIN_FINISH_DECL - Có thể in AST bằng
debug_treevà duyệt AST bằngwalk_tree_without_duplicates
- Plugin GCC có thể được nạp bằng
- Vấn đề là việc kiểm tra nhãn
casediễn ra trong quá trình GCC parsing- Plugin chỉ truy cập AST sau khi parsing kết thúc
- Nhãn lỗi như
case SIGTERM:sẽ không còn trong AST - GCC đã báo lỗi trước đó nên plugin khó sửa được
Thử nghiệm chặn macro tiền xử lý
- Vì chỉ dùng plugin thì khó ngăn lỗi trước lúc parsing, tác giả thử cách tương tác với C preprocessor
- Thông qua cấu trúc
cpp_readertrong header plugin GCC, có thể gắn callback khi macro được#define,#undefhoặc được sử dụng - Tận dụng cấu trúc macro giá trị hệ thống của Cosmopolitan Libc để chèn hằng số tạm thời
- Ví dụ:
extern const int SIGTERM; - Ví dụ:
#define SIGTERM SYMBOLIC(SIGTERM) - Với plugin, đặt các giá trị tạm như
static const int __tmpcosmo_SIGTERM = ...
- Ví dụ:
- Plugin chặn việc dùng
SYMBOLIC(SIGTERM), thay bằng giá trị tạm, rồi ở giai đoạnPLUGIN_PRE_GENERICIZElại sửa AST vềVAR_DECLđúng ban đầu - Cách hack macro này đã thành công trong việc lách lỗi của
switchvà khởi tạostruct - Theo cách này, ít nhất việc build CPython 3.11 cũng trở nên khả thi
Vì sao sửa thẳng GCC thay vì dùng plugin
- Sau khi thảo luận với Justine Tunney và xác nhận rằng vấn đề có thể giải quyết được, một hướng tiếp cận đơn giản hơn được đề xuất: sửa trực tiếp codebase GCC
- Plugin và hack macro tạo ra nhiều công việc không cần thiết, còn việc xử lý edge case và crash thì rất phức tạp
- Ví dụ, trong trường hợp chỉ cần biến đổi
case ENOSYS, việc thay macro lại buộc plugin phải xử lý cả những cách dùng hợp lệ nhưENOSYS + 1hayprintf("ENOSYS = %d", ENOSYS) - Với cách vá GCC, tính năng được bật bằng cờ
-fportcosmothay vì-fpluginriêng - Ngay trước khi GCC báo lỗi
case is not constant, nó sẽ kiểm tra xemflag_portcosmocó bật hay không; nếu có thì thực hiện phép thay thế cần thiết - Mã biến đổi AST của plugin cũ được chuyển vào bên trong GCC để chạy trước khi gọi các callback plugin khác
- Nhờ thay đổi này, có thể loại bỏ phần mã hack liên quan đến macro
Build thực tế và mở rộng bản vá
- Tính đến ngày 5/6/2023, GCC đã vá vượt qua toàn bộ test case của plugin trước đó, và binary mới đã được thêm vào monorepo Cosmopolitan Libc
- Khi biên dịch thêm nhiều mã hơn, bản vá tiếp tục được chỉnh sửa và cải thiện
- Việc build
luatrở nên đơn giản, đồng thời cũng xác định được nơig++phát sinh lỗicase constant, từ đó có thể build cảninja - Trong lúc cố gắn
ncursesvào Python 3.11, một vấn đề mới được phát hiệnncursescó đoạn mã đặt biểu thức nhị phân vào giá trị khởi tạo struct, nhưDATA2(PARODD | PARENB, PARODD)- Bản vá ban đầu xử lý được khởi tạo hằng số đơn giản nhưng chưa xử lý được các biểu thức như
PARODD | PARENB
- Sau đó, bản vá được mở rộng để cho phép nhãn
casevà các phần tử khởi tạo struct trong C nhận biểu thức tùy ý- Theo chuẩn C, nhãn
casevẫn phải là hằng số tại thời điểm biên dịch - Compiler đã vá sẽ phát cảnh báo với các cách dùng như vậy nhưng vẫn tiếp tục biên dịch
- C++ bị loại khỏi phạm vi thay thế biểu thức tùy ý vì không tương thích với xử lý
constexprcủag++11
- Theo chuẩn C, nhãn
- Sau thay đổi này,
ncursesđược build trơn tru
Những ràng buộc còn lại và kết luận thực dụng
- Bản vá không hoàn hảo
- Chưa xử lý được một số struct ẩn danh
- Chưa xử lý được
enumvà một phần trường hợp củaconst int - Chưa xử lý được các trường hợp dùng
SIGTERMlàm chỉ số mảng trong biểu thức khởi tạo - Vẫn có thể còn các crash hiếm gặp của compiler với các kiểu khởi tạo
statickỳ lạ hoặc khi đưaSIGTERMvàostatic const int8_t
- Dù vậy, nhiều phản ví dụ rõ ràng đã được loại bỏ, và nhiều phần mềm phổ biến có thể build mượt mà
- Môi trường kiểm thử nghiêm ngặt hơn có thể tiếp tục làm lộ ra các điểm cần cải thiện
- Nếu muốn, vẫn có thể tự sửa tay các câu lệnh
switchvà biểu thức khởi tạostruct, nhưng trong nhiều trường hợp compiler có thể tự xử lý - Nếu phần mềm C có thể được build tĩnh, đặc biệt là với
musl, thì nhiều khả năng cũng có thể build được với Cosmopolitan Libc
3 bình luận
Bây giờ neo chuyển sang định dạng bọc tin nhắn trong khối mã rồi à? Việc phải cuộn ngang và màu chữ tông đỏ thì hơi đáng tiếc...
Ôi, việc bị bọc trong khối mã là lỗi đó hu hu
Ý kiến trên Hacker News
Tôi là người viết bài này, và tiêu đề đúng ra phải là “Patching GCC to build Actually Portable Executables”. Vì nó đang nói đến định dạng Actually Portable Executable của Cosmopolitan Libc và jart.
Với bản vá GCC của tôi, giờ đây có thể build các phần mềm như vim, emacs, ninja, bash, git, gcc bằng Cosmopolitan Libc thông qua hệ thống build kiểu autotools/cmake thông thường. Các file thực thi được build ra sẽ có thể chạy trên Linux, FreeBSD, MacOS, OpenBSD, NetBSD và Windows. Tuy nhiên, Windows thì tôi vẫn chưa thử nghiệm.
Danh sách phần mềm tôi đã thử build bằng kỹ thuật này có ở đây: https://github.com/ahgamut/superconfigure
Script superconfigure chỉ là một wrapper bọc quanh script configure thông thường dùng để build phần mềm, rồi thêm các cờ như
--enable-static.Nếu muốn thử build GCC bằng Cosmopolitan Libc, bạn có thể dùng kho này: https://github.com/ahgamut/musl-cross-make/tree/gccbuild
Hy vọng ở HN bạn sẽ được đối xử tốt hơn
Ví dụ, tôi muốn biết có đủ mức kiểm thử để chứng minh rằng không có hồi quy so với bản glibc hay không
Có một issue liên quan đến việc này trong kho Go: https://github.com/golang/go/issues/51900
Nhìn chung khá ngầu, nhưng tôi tò mò về cơ sở lý thuyết của nó. Nếu tạo một mục tiêu hệ điều hành mới, về mặt logic thì có vẻ đúng hơn là định nghĩa một bộ hằng số mới.
Thay vì chuyển nguyên các hằng số của từng hệ điều hành con, có thể ánh xạ chúng để hằng số của mỗi hệ điều hành trở nên giống nhau ở mọi nơi. Khi đó, mã dùng các hằng số làm chỉ số mảng hoặc trong câu lệnh case cũng sẽ hoạt động bình thường.
Cách này có thể tạo thêm overhead so với phương pháp trong bài, nhưng có vẻ dễ hơn nhiều và cũng đơn giản hơn cho những ai muốn biên dịch chương trình mới với Cosmopolitan libc
Có thể xem danh sách hằng số ở đây: https://github.com/jart/cosmopolitan/blob/master/libc/sysv/c...
Mỗi lần dùng một trong các hằng số này, bạn sẽ phải đưa rất nhiều hằng số vào binary dưới dạng bảng tra cứu lớn, và mỗi khi chương trình cần kiểm tra thì lại phải đi qua bảng đó. Có thể không quá chậm, nhưng theo tôi chắc chắn sẽ cảm nhận được.
Mục tiêu là giúp việc porting trở nên dễ dàng mà không phải sửa nhiều mã nguồn của libc hay phần mềm cần port, đồng thời tạo ra các binary có hiệu năng tương đương hoặc tốt hơn. Trong những ràng buộc đó, bản vá GCC này có vẻ là cách tốt nhất để đơn giản hóa quy trình.
Nếu tôi gặp đủ nhiều codebase dùng
SIGHUPđể khởi tạo chỉ số mảng, tôi sẽ thử cách bạn đề xuất để đo xem điểm đánh đổi nằm ở đâu. Hoặc bạn cũng có thể tự thử và cho tôi biết liệu một bộ hằng số riêng có tốt hơn khôngNếu đúng là “đã dành khá nhiều thời gian để loại bỏ các phản ví dụ hiển nhiên và nhiều phần mềm phổ biến build trơn tru”, thì việc công khai danh sách phần mềm compile thành công sẽ rất hữu ích. Có thể danh sách đó đã tồn tại rồi
Script superconfigure chỉ là một wrapper bọc quanh script configure thông thường dùng để build phần mềm, rồi thêm các cờ như
--enable-static.Tôi thắc mắc liệu APE có mục đích thực tiễn nào không. Có nơi nào thực sự phân phối một tệp thực thi duy nhất phải chạy được trên nhiều hệ điều hành không?
Bản thân lnav không được build bằng APE, nhưng agent nhúng bên trong thì được build bằng APE. Khi người dùng muốn đọc log của host đó, agent sẽ được gửi từ xa đến host
Làm vậy không cần thêm bước nhận diện riêng loại hệ điều hành hay đóng gói nhiều phiên bản tệp thực thi. Có một bài ngắn liên quan ở đây: https://lnav.org/2021/05/03/tailing-remote-files.html
Các chương trình biên dịch tĩnh nằm ở tầng nền tảng của mọi thứ, nhưng ở cấp độ ứng dụng thì lại là thiểu số
APE đặt ra câu hỏi lớn: “Nếu một ngôn ngữ AOT cấp thấp như C có thể biên dịch một lần và chạy ở mọi nơi, thì ai còn cần máy ảo nữa?”
Hơn nữa, cross-compilation là một nỗi đau. Các dự án mã nguồn mở nhỏ không có build farm với đủ mọi BSD thường hoàn toàn không phát hành binary cho các nền tảng đó
Ngay cả kubectl cũng có binary cho Linux nhưng không có cho BSD. Trừ khi bạn tính cả OSX là BSD. Vì vậy với những dự án như thế, điều này có vẻ là một lợi ích dễ thấy
Nó quá lớn để build tại chỗ trên máy người dùng. Nó có nhiều phụ thuộc C++ bên thứ ba lớn, và thời gian build mất từ 2 đến 20 phút tùy phần cứng
Nó cần chạy trên macOS x86-64+ARM, Linux x86-64, Windows x86-64. Nếu được cả ARM Linux thì càng tốt
Hiện giờ tôi đang xem xét WASM, nhưng nó yêu cầu phải có sẵn runtime WASI đã cài đặt
Dành cho ai còn thấy khó hiểu: “Actually Portable Executables” https://justine.lol/ape.html
Việc đổi
switchthànhifkhá thô. Có lẽ có thể sửa bằng cách ánh xạ sang một tập hằng số trung lập nền tảng rồi dereference symbol runtime qua hàmVí dụ dùng kiểu
switch(CosmoMappErrno(EINVAL))vàcase COSMO_EINVAL:thì sẽ không biến mã thành một mớ gotoTất nhiên, nếu ý bạn là compiler sẽ nhận ra những
switchnhư thế và thay vì viết lại thành cây if, nó sẽ đổi label thành các hằng đặc biệt dành riêng cho cosmo cho từng giá trị, rồi bọc giá trị đầu vào của switch trong một lời gọi hàm ánh xạ từ giá trị nền tảng runtime hiện tại sang hằng dành riêng cho cosmo tương ứng, còn các giá trị khác thì giữ nguyên và cho đi qua, thì lại là chuyện khácĐiều đó thực ra có thể còn là một phép biến đổi compiler đơn giản hơn. Chỉ là sẽ phát sinh độ phức tạp do phải nhận ra đang dùng tập hằng nào trong số nhiều tập khác nhau, rồi áp dụng lời gọi ánh xạ thích hợp cho đầu vào của switch. Phía thư viện cũng phải tạo các ánh xạ kiểu này nên cũng cần thêm độ phức tạp
Đã có một triển khai hoạt động theo cách biến đổi khác rồi, nên tôi không biết tác giả có muốn triển khai cái này hay không
Cuối cùng, kiểu ánh xạ này có rủi ro ánh xạ sai, và còn có những rắc rối như với mã dùng giá trị errno âm thì đầu vào phải bị đảo dấu trước khi ánh xạ
Lý do tôi thích mảng if-else-goto là vì nó khớp rất tự nhiên với các phần khác của GCC. Nếu xem patch của tôi[1], bạn sẽ thấy để thêm tính năng này tôi chỉ phải chỉnh sửa rất ít mã hiện có của GCC
[1]: https://github.com/ahgamut/gcc/tree/portcosmo-11.2
Tôi không hiểu vì sao mọi người lại ngạc nhiên đến vậy với mã dùng
SIGxxxlàm chỉ số. Số hiệu tín hiệu được định nghĩa khá rõ, nhìn chung là liên tiếp và thường được biết là bắt đầu từ 1Hơn nữa lỗi đó xuất hiện cùng với lỗi compiler trong patch của tôi, thứ đã khiến tôi mất rất nhiều thời gian để debug, nên đó là một sự ngạc nhiên rất đáng nhớ
Tôi muốn biết liệu bạn có xem kiểu mẫu mã đó là phổ biến không. Tôi cũng muốn biết ngoài busybox còn codebase nào có kiểu đó. Nếu có nhiều ví dụ, tôi có thể dành thời gian cập nhật patch để xử lý kiểu mẫu đó
Nếu errno là vấn đề lớn đến thế, tôi không biết còn hy vọng nào cho việc chạy thư viện chuẩn C++ và std::error_code
Ở đó không có cách nào dùng các macro ma thuật
std::error_codekhông phải là hằng, nên có lẽ sẽ không gặp cùng vấn đề