3 điểm bởi GN⁺ 2023-07-15 | 3 bình luận | Chia sẻ qua WhatsApp
  • Để 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, EINVAL khô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ãn switch case và khởi tạo static/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ãn case xả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ến switch thành if/goto ngay 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á gcc khoả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 ./configure hoặc cmake để build các phần mềm sau
    • bash
    • curl
    • git
    • ninja
    • 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, EINVAL trong nhãn case của câu lệnh switch sẽ xung đột với quy tắc biên dịch C
  • Chuẩn C yêu cầu nhãn case phả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 ifgoto

  • Nếu các nhãn như case EINVAL, case ENOSYS bên trong switch(errno) không phải hằng số tại thời điểm biên dịch, GCC sẽ báo lỗi case label does not reduce to an integer constant
  • Bản vá tự động biến đổi câu lệnh switch thành cấu trúc sau
    • Với mỗi giá trị case, thêm một nhánh dạng if (errno == EINVAL) goto caselabel_EINVAL;
    • Thêm goto để chuyển tới nhãn default
    • Khối case gốc được dựng lại bằng nhãn và goto endofthis_switch
  • 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 switch trộn ifdef và fallthrough, phép biến đổi dựa trên goto là 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 static hoặc const struct
  • Nếu EINVAL khô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 struct cục bộ thông thường bên trong hàm thì vẫn được
    • const struct
    • static struct
    • const struct toàn cục
    • static struct toàn cục
  • Một ví dụ thực tế là module faulthandler củ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

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 ifdef nào sẽ được kích hoạt
  • 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_tree và duyệt AST bằng walk_tree_without_duplicates
  • Vấn đề là việc kiểm tra nhãn case diễ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_reader trong header plugin GCC, có thể gắn callback khi macro được #define, #undef hoặ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 = ...
  • Plugin chặn việc dùng SYMBOLIC(SIGTERM), thay bằng giá trị tạm, rồi ở giai đoạn PLUGIN_PRE_GENERICIZE lạ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 switch và khởi tạo struct
  • 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 + 1 hay printf("ENOSYS = %d", ENOSYS)
  • Với cách vá GCC, tính năng được bật bằng cờ -fportcosmo thay vì -fplugin riêng
  • Ngay trước khi GCC báo lỗi case is not constant, nó sẽ kiểm tra xem flag_portcosmo có 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 lua trở nên đơn giản, đồng thời cũng xác định được nơi g++ phát sinh lỗi case constant, từ đó có thể build cả ninja
  • Trong lúc cố gắn ncurses vào Python 3.11, một vấn đề mới được phát hiện
    • ncurses có đ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 case và 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 case vẫ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ý constexpr của g++11
  • 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 enum và một phần trường hợp của const int
    • Chưa xử lý được các trường hợp dùng SIGTERM là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 static kỳ lạ hoặc khi đưa SIGTERM vào static 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 switch và biểu thức khởi tạo struct, 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

 
ryuheechul 2023-07-15

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...

 
xguru 2023-07-16

Ôi, việc bị bọc trong khối mã là lỗi đó hu hu

 
GN⁺ 2023-07-15
Ý 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

    • Tôi tò mò liệu bản vá này có khả năng được đưa vào upstream GCC hay không. APE executable và cosmopolitan libc là công nghệ cực kỳ tuyệt vời, nên sẽ rất tốt nếu chúng dễ sử dụng hơn
    • Hay đấy. Tôi tự hỏi liệu có khả năng phát sinh vấn đề khi dùng cái này phía sau các ngôn ngữ transpile như Nim hay không
    • Không biết bạn đã thử build busybox chưa
    • Gần đây tôi thấy trên lobste.rs có chuyện bạn bị chặn vì bị cho là sockpuppet của jart, giờ mới nhận ra bạn thực sự là người khác.
      Hy vọng ở HN bạn sẽ được đối xử tốt hơn
    • Chỉ build được thôi thì chưa hẳn là thành tựu lớn đến vậy, điều thú vị hơn là liệu nó có thực sự chạy tốt không.
      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

    • Tạo hằng số mới sẽ giúp việc compile dễ hơn nhiều, nhưng cá nhân tôi thấy overhead của việc chuyển đổi qua lại giữa hằng số cũ và mới trong lúc chạy là quá lớn.
      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ông
  • Nế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

    • Có thể build các phần mềm như vim, emacs, ninja, bash, git, gcc. Danh sách 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.
  • 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?

    • Logfile Navigator(https://lnav.org) dùng tệp thực thi APE làm agent giao tiếp với host từ xa
      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
    • Tôi nghĩ ngay đến các lập trình viên Java/Clojure/Kotlin, lập trình viên .Net, JS, Python, Ruby, Erlang và mọi ngôn ngữ dựa trên máy ảo
      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?”
    • Ví dụ, ở những nơi bất tiện khi phải có nhiều artifact cho cùng một tổ hợp id/version như kho Maven, điều này có vẻ sẽ giúp việc phân phối tệp thực thi dễ hơn đôi chút. Có lẽ PyPI cũng gặp vấn đề tương tự
      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
    • Từ giờ tôi định phát hành các side project theo cách đó. Tôi cũng build tệp thực thi Redbean cho cả SPA chỉ phía client. Ý tưởng những thứ này cứ thế chạy trên nhiều hệ thống thật đáng kinh ngạc
    • Có một trình biên dịch shader sẽ được lợi nếu tồn tại định dạng tệp thực thi có thể chạy ngay, không phụ thuộc nền tảng
      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

    • Tôi đã thêm một bình luận nhắc đến chuyện này nhưng vẫn chưa được đổi. Tôi nghĩ rồi nó sẽ sớm được đổi thôi. Không biết gọi @dang ở đây có được không nhỉ?
  • Việc đổi switch thành if khá 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àm
    Ví dụ dùng kiểu switch(CosmoMappErrno(EINVAL))case COSMO_EINVAL: thì sẽ không biến mã thành một mớ goto

    • Nhưng như vậy cũng không giúp biên dịch mã chưa sửa đổi. Các goto do compiler tạo ra thì không cần lo; compiler vốn vẫn làm vậy ở nhiều chỗ
      Tất nhiên, nếu ý bạn là compiler sẽ nhận ra những switch như 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ạ
    • Cách dereference symbol runtime qua hàm nghe khá thú vị. Nếu bạn chỉ cho tôi một ví dụ chạy được thì tôi sẵn sàng thử
      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
    • Có cần phải bận tâm cả chuyện việc thay thế có diễn ra tự động hay không không?
  • 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 SIGxxx là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ừ 1

    • Trước khi thử busybox, tôi đã build nhiều codebase như git, bash, gcc, curl và chưa từng thấy kiểu mã như vậy
      Hơ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

    • Có vẻ vấn đề nằm ở chính những macro ma thuật đó. Bình thường macro mở rộng thành hằng, nhưng giờ thì không còn thế nữa nên một số thứ bị hỏng
      std::error_code không phải là hằng, nên có lẽ sẽ không gặp cùng vấn đề