1 điểm bởi GN⁺ 2025-01-10 | 1 bình luận | Chia sẻ qua WhatsApp
  • SerenityOS chủ yếu chạy tốt trên QEMU, nhưng trên laptop thật, các vấn đề lần lượt xuất hiện ngay từ khâu khởi động, debug và truy cập storage, làm lộ ra những khoảng trống trong hỗ trợ phần cứng
  • Thiết bị thử nghiệm là Dell 3100 Chromebook với Intel Celeron N4020, 4GB DDR4, eMMC 32GB và màn hình TN 1366×768; cơ chế debug closed-case dựa trên Cr50 như kỳ vọng đã thất bại trên bo mạch này
  • Khi đường Cr50 bị chặn, tác giả đặt một Pi Pico dùng RP2040 vào bên trong, nối trực tiếp tới UART và SPI flash, rồi dùng CircuitPython cùng serprog để tạo một thiết bị debug và flash tạm thời tên PicoCCD
  • Log khởi động ban đầu được thu thập bằng cách tận dụng IO port 0x80 mà ChromeOS EC ghi lại như một kênh xuất tạm thời chậm, vì khó dùng ngay UART MMIO 16550 nằm sau PCI
  • Hỗ trợ eMMC đã vượt qua các khác biệt trong khởi tạo SD/MMC, thiếu sót điều khiển nguồn SDHCI và vấn đề vô hiệu hóa lệnh chỉ dành cho SD để đi tới được một phần phiên đồ họa, nhưng hiệu năng, độ ổn định và việc dọn dẹp patch vẫn còn phía trước

Dell 3100 Chromebook được chọn làm mục tiêu phần cứng thật

  • Trong quá trình muốn tham gia sâu hơn vào SerenityOS, điểm yếu đầu tiên đập vào mắt là nó chạy được trên QEMU nhưng hỗ trợ phần cứng thật còn thiếu
  • Hỗ trợ UEFI đã được spholz triển khai, nên tác giả không động vào; với công việc này, chỉ cần kernel nhánh master khởi động bằng GRUB trong runtime TianoCore UEFI là đủ
  • Tác giả muốn tránh cách debug OS trên cùng thiết bị với máy phát triển chính, và chọn phần cứng tương đối mới đến mức có thể dùng hằng ngày trong thực tế
  • Khi tìm một Chromebook giá rẻ trên Allegro, tác giả mua được Dell 3100 với giá 95PLN, khoảng 25EUR
    • Intel Celeron N4020, 2 nhân, không có Hyper-Threading
    • 4GB DDR4
    • eMMC onboard 32GB
    • Màn hình TN 1366×768 do IGP UHD600 điều khiển
    • 2 cổng USB-A, 2 cổng USB-C, jack 3,5mm
    • Bàn phím được cảm nhận là tốt hơn các laptop doanh nghiệp cao cấp của Dell
  • Thiết bị này về sau được gọi bằng hostname octopus

Kỳ vọng debug dựa trên Cr50 và thất bại

  • Một lý do lớn để chọn Chromebook là chip bảo mật kiêm embedded controller Cr50 cung cấp các tính năng hữu ích cho debug closed-case
  • Gần như mọi Chromebook từ năm 2018 trở đi đều có thể dùng một cổng USB-C để debug bằng cáp SuzyQ, và Cr50 thường phơi ra ba thiết bị ttyUSB
    • Console nội bộ của Cr50
    • Console AP, tức cổng serial của Chromebook
    • Console cros_ec, tức embedded controller
  • Mục tiêu là truy cập serial console mà không phải mở laptop rồi để dây lòng thòng; nếu tính cả giả lập nhập phím và điều khiển trạng thái nguồn của cros_ec, một dạng KVM đơn giản cũng có vẻ khả thi
  • Trên octopus thực tế, Cr50 CCD không hoạt động
    • Tác giả làm một cáp SuzyQ mới và kiểm chứng mối hàn bằng Chromebook khác nhưng vẫn thất bại
    • octopus là một trong số ít laptop mà CCD không hoạt động vì Dell không gắn một số điện trở trên bo mạch
    • Một số người nói họ thành công hạn chế trong điều kiện hướng cắm cổng cụ thể và có cắm sạc, nhưng trên thiết bị này thì hoàn toàn không hoạt động
  • Theo thông tin kiểm tra sau đó, điện trở bị thiếu lẽ ra chỉ ảnh hưởng đến SPI flashing chứ không phải bản thân USB bridge, nên lý do chính xác khiến debug Cr50 hoàn toàn không hoạt động vẫn chưa rõ

PicoCCD được làm bằng Pi Pico

  • Sau khi đường Cr50 bị chặn, tác giả kiểm tra xem có thể đặt một bo Pi Pico thông thường vào khoảng trống bên trong thiết bị hay không, và thấy là vừa đủ
  • Tác giả tham khảo sơ đồ mạch của các laptop tương tự, nhưng không có sơ đồ nào khớp chính xác với octopus
    • Một cổng debug lớn trên bo là các test point và JTAG liên quan đến Intel, không phù hợp với mục đích
    • Cổng còn lại là Google Servo, nhưng Google đã phát hành nhiều debug probe dưới tên Servo và tài liệu cũng hạn chế nên khó tra cứu
    • Tác giả tham khảo tài liệu Servo
  • Tác giả bật applet UART và phát hiện tần số của Glasgow, rồi trực tiếp dò các pad UART TX nghi vấn trong lúc Linux liên tục xuất ra /dev/ttyS1
    • Pad TX được tìm thấy trong vài phút
    • RX khó hơn vì cần truyền chủ động, và chạm nhầm đường có thể làm bo mạch reset; thực tế đã xảy ra reset hai lần
    • Sau đó, các chân RX/TX cho EC cũng được tìm thấy trong khoảng 10 phút
  • Các dây đã hàn được cố định bằng epoxy đóng rắn bằng UV, và trong 6 tháng không gặp vấn đề kết nối
  • Tác giả cũng tận dụng ngoại vi SPI của RP2040 để hàn 6 dây vào chip flash, cắt trace đi tới chân write-protect rồi nối xuống GND để có quyền ghi mà không cần Cr50 cho phép
  • Về phần mềm, tác giả chọn CircuitPython
    • Vì có thể tải script và dữ liệu lên dưới dạng USB mass storage
    • Việc bridge UART thành thiết bị USB cdc_acm khá đơn giản
    • Vì đã nối cả SPI flash nên cũng cần chức năng flashing
  • Tác giả dùng flashrom làm công cụ flashing EEPROM/SPI mã nguồn mở phổ biến, và serprog, vốn proxy SPI qua UART, phù hợp với mục đích
    • Đã có bản triển khai C pico-serprog của stacksmashing, nhưng việc phải flash lại Pico mỗi lần flash BIOS không phù hợp
    • Thay vào đó, tác giả triển khai serprog bằng CircuitPython và tham khảo nhiều từ applet Glasgow serprog
  • Mã kết quả được sắp xếp thành giải pháp debug closed-case làm nhanh tên PicoCCD, và repository nằm tại PicoCCD trên Forgejo
  • WeirdTreeThing cũng viết mã RP2040 C với mục đích tương tự, và cũng có phiên bản PicoCCD của anh ấy

Thu thập log khởi động SerenityOS

  • Tác giả cài Alpine Linux để debug và cấu hình các tiện ích cơ bản để đưa kernel SerenityOS được build bên ngoài vào máy
  • Sau đó, cấu trúc này lớn dần thành hệ thống tự động tải artifact từ máy build, giải nén và ghi đè kernel, cũng như có entry GRUB để giải nén .tar của user space
  • Thời gian lặp từ lúc thay đổi đến lúc test, tại thời điểm viết, khoảng 20 giây, khá ổn đối với việc hack bare-metal
  • Entry khởi động GRUB đầu tiên thực chất là multiboot /Kernel serial_debug, nhưng cả màn hình lẫn cổng serial đều không có đầu ra nào
  • Với vấn đề xuất màn hình, tác giả tìm ra cách thêm insmod all_video vào entry khởi động GRUB; riêng việc đó chưa giải quyết được, nhưng là hướng đúng
  • Việc không có đầu ra serial là vấn đề lớn hơn
    • Log coreboot vẫn còn vào được cho đến vài giây trước đó
    • UART của thiết bị này không phải 16550 kiểu port-mapped truyền thống mà là 16550A dựa trên MMIO
    • Log Linux hiển thị ttyS0ttyS1 là 16550A nằm tại địa chỉ MMIO
    • Trong lspci, Intel Celeron/Pentium Silver Processor Serial IO UART Host Controller xuất hiện như một thiết bị PCI

UART 16550 và đường vòng port 0x80

  • Theo truyền thống, thiết bị ngoại vi của IBM PC được map vào port I/O của CPU x86 và được truy cập bằng các lệnh như outbinb
  • Nhiều thiết bị về sau chuyển sang MMIO, nhưng cổng serial vẫn giữ cách cũ vì cạnh tranh tốc độ cao không quan trọng, nên hữu ích làm cổng debug
  • Trong môi trường thông thường, viết kiểu outb 0x3f8, 0x41 là phía bên kia có thể nhận được A, và quá trình khởi tạo cũng ngắn nên dễ triển khai trong các dự án nhỏ
  • UART của octopus là thiết bị MMIO nằm sau PCI, và ở giai đoạn rất sớm khi SerenityOS khởi động thì khó kỳ vọng PCI đã được khởi tạo
    • SerenityOS có triển khai bus PCI, nhưng thời điểm khởi động còn quá sớm
    • PCISerialDevice hiện có cũng chưa từng được dùng trong ngữ cảnh MMIO
    • Việc viết driver này mà không có đầu ra debug là không lý tưởng
  • Embedded controller của thiết bị ChromeOS ghi log mọi lệnh ghi tới IO port 0x80
    • Port này theo truyền thống được dùng để báo trạng thái POST
    • Bộ hiển thị mã khởi động 7 đoạn trên bo mạch chủ hoạt động bằng cách decode port 80
  • Tác giả kiểm thử giả thuyết bằng một script ghi byte vào /dev/port trên Linux, và có thể đọc được byte đó trên console cros_ec
  • Tác giả chèn mã như IO::out8(0x80, 1); quanh điểm bắt đầu của SerenityOS là Kernel/Arch/init.cpp để theo dõi vị trí thực thi, rồi thu hẹp được điểm crash tới Memory::MemoryManager::initialize(0);
  • Sau đó, tác giả thử đổi địa chỉ của routine ghi serial từ 0x3f8 sang 0x80
    • Ban đầu có nhiều byte xuất ra, nhưng cros_ec không relay ổn định được nên xảy ra overflow
    • Sau đó nữa, chính đầu ra log cros_ec cũng bị hỏng
    • Lý do là nó không có buffer lớn như chip serial thật
  • Tác giả đi vòng bằng cách chèn nhiều wait state dựa trên nop giữa mỗi lần ghi
    • Nếu in hết thông điệp khởi động, quá trình boot từ vài giây tăng lên vài phút
    • Dù vậy, tác giả xem đây là cái giá chấp nhận được cho debug bare-metal
  • Để tự động parse dòng log cros_ec và decode thành ASCII, tác giả dùng một lệnh Bash một dòng kết hợp picocom, watch, grep, sed, cut, xxd

Framebuffer và đầu ra đồ họa đầu tiên

  • Sau khi có log khởi động, tác giả dành vài ngày đọc trực tiếp codebase để hiểu vấn đề, nhưng cuối cùng nhờ cộng đồng trợ giúp
  • spholz chỉ ra SerenityOS PR #24435 đang mở lúc đó; khi build từ nhánh này, generic framebuffer hoạt động
  • Trên màn hình xuất hiện kết quả có trạng thái như thể việc thất bại đã thành công, và sau đó vấn đề storage bắt đầu lộ rõ

eMMC và vấn đề khởi tạo SD/MMC

  • Lỗi crash StorageManagement hiện có dẫn tới một assertion rằng danh sách controller rỗng sau khi khởi tạo SD Host Controller thất bại
    • Log cho thấy PCI: Failed to initialize SD Host ControllerASSERTION FAILED: !m_controllers.is_empty()
    • Kết quả là kernel panic xảy ra trong StorageManagement::enumerate_storage_devices()
  • octopus có chip eMMC 32GB, và SerenityOS đã có một phần driver SD nên có vẻ chỉ cần thêm hỗ trợ MMC
  • Để dùng thẻ SD/MMC, về cơ bản cần ba yếu tố
    • Host Controller: trên thiết bị hiện đại thường là SDHCI do SD Association đặc tả
    • Bus kết nối với Host Controller: trong trường hợp này là PCI
    • Triển khai protocol để host và card giao tiếp
  • Theo log crash, SerenityOS đã có hai yếu tố đầu, vấn đề còn lại nằm ở phần protocol
  • SD protocol có đặc tả công khai, nhưng MMC sau khi trở thành tiêu chuẩn JEDEC năm 2007 thì cần trả phí để truy cập chính thức
  • SD và MMC có trình tự khởi tạo khác nhau
    • SerenityOS bắt đầu bằng cách gửi CMD0 và chờ phản hồi, điều này lẽ ra phải qua được với cả SD và MMC
    • Tiếp theo, nó gửi CMD8 để thiết lập điện áp, nhưng MMC không hỗ trợ lệnh này nên phải báo lỗi
    • Một số tài liệu đề xuất sau đó reset card và xem nó là MMC
    • Tài liệu khác đưa ra luồng toàn diện hơn, phân loại cả phiên bản SD và loại dung lượng bằng tổ hợp kết quả CMD8 và CMD58
  • Tác giả không triển khai toàn bộ kiểm tra tương thích nâng cao mà chỉ triển khai kiểm tra cơ bản

Thiếu thanh ghi điều khiển nguồn và cách giải quyết

  • Luồng khởi tạo MMC có thể tóm tắt như các bước sau
    • Sau reset, đặt clock thành 400KHz
    • Chờ 1ms rồi chờ thêm 74 clock
    • Gửi CMD0 và chờ phản hồi
    • Lặp CMD1 cho tới khi bit thứ 31 của phản hồi bằng 1
    • Khi vòng lặp kết thúc, lưu giá trị làm thanh ghi Operating Conditions
    • Tiếp tục thuật toán khởi tạo SD, trừ việc truy vấn các thanh ghi chỉ dành cho SD
    • Tùy chọn phát hiện tương thích High-Speed và bật một trong nhiều chế độ HS
  • Code đi được tới khoảng bước 4, nhưng sau đó eMMC không phản hồi bất kỳ yêu cầu nào
  • Sau vài ngày không tìm ra nguyên nhân, tác giả gỡ mã liên quan đến reset controller thì eMMC bắt đầu phản hồi, và vấn đề được thu hẹp vào hàm reset_host_controller()
  • Điểm lạ của hàm này là không thể tìm thấy thanh ghi host_configuration trong tiêu chuẩn
    • Code trước đó đã tùy ý gom nhiều thanh ghi thành hai nhóm host_configuration
    • Quá trình khởi tạo cũng chưa hoàn tất, và nhóm đầu tiên chỉ đơn giản bị đặt về 0
  • Nhóm đầu tiên bao gồm thanh ghi Power Control điều khiển bộ điều áp nguồn của card
    • Trên một số phần cứng, gồm mọi triển khai dùng eMMC, thanh ghi này cần thiết để bật chính card
    • Ở các thiết kế khác nối trực tiếp rail nguồn vào slot, thiết lập này có thể bị bỏ qua
  • Là giải pháp tạm thời, tác giả lấy và dùng giá trị vốn có trong host_configuration_0
  • Vấn đề cốt lõi là đã cố giao tiếp với card khi chưa bật nguồn cho nó
  • Sau đó, tác giả mất thêm vài giờ để tìm và vô hiệu hóa một số lệnh chỉ hợp lệ với thẻ SD; khi controller bắt đầu xuất debug có ý nghĩa hơn, phần việc còn lại diễn ra tương đối bình thường

Trạng thái hiện tại và việc còn lại

  • Cuối cùng, SerenityOS đã thành công mở một phiên đồ họa rất chậm và bị hỏng một phần, nhưng nhanh chóng bị treo
  • Vấn đề phiên đồ họa này và quá trình đưa framebuffer trở lại bình thường sẽ được nói trong bài tiếp theo
  • Toàn bộ công việc là một quá trình học tập kéo dài khoảng 6 tháng, trong thời gian đó tác giả cũng làm những việc khác
  • Mục tiêu tiếp theo là dọn dẹp các patch và đưa lên upstream trong năm nay

1 bình luận

 
GN⁺ 2025-01-10
Ý kiến trên Hacker News
  • Tôi từng đọc rằng việc điều chỉnh driver của NetBSD cho phù hợp với kernel tùy chỉnh tương đối dễ, nên tự hỏi liệu phía Serenity cũng có thể đi theo hướng đó không
    Với một OS mới, driver thiết bị là một rào cản lớn

    • Một trong các triết lý của Serenity là tự làm mọi thứ từ đầu nhiều nhất có thể, nên dù driver NetBSD dễ điều chỉnh và license tương thích, có lẽ họ vẫn sẽ tự viết driver hơn là chọn con đường đó
    • Tôi từng thắc mắc liệu với một OS mới hoặc OS sở thích cá nhân, ngay từ đầu nhắm tới một máy tính bo mạch đơn phổ biến như Raspberry Pi có phải tốt hơn không
      Vì cấu hình phần cứng gần như cố định, việc viết hoặc lấy driver và kiểm thử hệ thống sẽ dễ hơn
    • rump kernel/anykernel chính là khái niệm như vậy
      Có thể chạy driver trong user space chỉ với phần hỗ trợ nền tảng tối thiểu
      https://en.wikipedia.org/wiki/Rump_kernel
    • Phía NetBSD rất đáng tin. libc của họ rất gọn gàng nên tôi đã vài lần mang dùng trong các dự án khác
      Tuy nhiên về phần driver thì tôi không rõ lắm
    • Cách giải là chọn một bộ phần cứng tốt, tốt nhất là thiết bị do chính người viết phần mềm bán, rồi chỉ làm driver cho phần cứng đó
      Tôi thấy Apple về cơ bản đã làm như vậy ngay từ đầu, và đó là trường hợp duy nhất thành công lớn trong dòng Unix dành cho người tiêu dùng
      System76 cũng gần như là một ví dụ như thế, Frame.work cũng tương tự nhưng ít tập trung vào bản thân OS hơn
  • Việc khiến nó chạy được trên một máy có mọi điều kiện đều bất lợi thật sự là một màn hack rất ấn tượng, có vẻ là kết quả của nỗ lực khổng lồ từ những người tài năng

  • Đọc những bài như thế này khiến tôi tự hỏi nên bắt đầu bước vào thế giới driver và OS như thế nào
    Trông quá phức tạp nên tôi không rõ nên bắt đầu từ đâu

    • Giao tiếp với phần cứng hiện đại thực ra khá đơn giản; cốt lõi là đọc và ghi bộ nhớ của phần cứng
      Việc này được gọi là vào/ra ánh xạ bộ nhớ (MMIO). Trong ứng dụng thông thường thì không làm được vì kernel chặn truy cập trực tiếp vào bộ nhớ phần cứng
      Để bắt đầu, bạn cần một ngôn ngữ có thể sinh mã máy cho CPU mục tiêu, như Rust/C++/C/Zig, và tốt nhất là không có runtime hay GC. Nếu đây là lần đầu với ngôn ngữ cấp thấp, tôi khuyên dùng C vì có nhiều ví dụ
      Bạn cũng cần học assembly cơ bản của CPU mục tiêu, vì một số lệnh có thể không được cung cấp dưới dạng hàm dựng sẵn trong ngôn ngữ cấp cao
      Sau đó, trong khi viết kernel hello world, bạn sẽ học CPU khởi động kernel như thế nào, các chế độ thực thi và mức đặc quyền được phân chia ra sao
      Tiếp theo, trên x86, bạn cấu hình CPU theo cách mình muốn, chẳng hạn chuyển sang long mode để dùng lệnh 64-bit; thường ở giai đoạn này cũng sẽ thiết lập bộ nhớ ảo
      Đến đây bạn sẽ bắt đầu nắm được CPU khớp với OS ra sao, cách liệt kê các thiết bị khả dụng và tìm vị trí bộ nhớ; sau đó vẫn còn nhiều việc như file system, scheduler
      Khác biệt giữa phần mềm chạy trên OS và kernel của OS rốt cuộc là chế độ CPU hiện đang chạy đoạn mã đó, và ở mức đặc quyền cao nhất có thể dùng các lệnh mà ứng dụng thông thường không dùng được
    • Tôi bắt đầu với LDD. Đây là cuốn sách khoảng 10 năm tuổi nhưng đến nay vẫn còn liên quan
      Về sau tôi tìm thấy những tài liệu như kho báu ẩn trong tài liệu FreeBSD, trong đó FreeBSD Architecture Handbook và FreeBSD Developers' Handbook có thể đặc biệt hữu ích
      https://lwn.net/Kernel/LDD3/
      https://docs.freebsd.org/en/books/
    • Khoảng 15 năm trước tôi học một môn phát triển OS ở đại học và dùng Minix
      Minix được viết rất sạch, kernel cũng chỉ khoảng 5 nghìn dòng và được trình bày trong nhiều giáo trình
      Tôi đã triển khai một server đơn giản và cũng hack kernel; vì Minix là microkernel nên hầu hết driver hoạt động theo kiểu đó
      Tôi đọc trước tài liệu môn học và gần như không nghe giảng mà vẫn được 8/10 điểm
      Tôi cũng nghe nhiều điều tốt về NetBSD và SerenityOS, còn Andreas đã phát triển rất nhiều qua livestream
      Khi biết bắt đầu từ đâu thì thực ra mọi thứ trở nên dễ hơn
    • Thành thật mà nói, bước đầu tiên là hiểu mục đích của từng thành phần như OS, driver, thiết bị
      Ví dụ, driver thiết bị có nhiệm vụ cung cấp một giao diện để các chương trình khác chạy trên máy tính có thể truy cập và điều khiển một thiết bị nào đó
      https://m.youtube.com/watch?v=juGNPLdjLH4 là một bài giảng cấp tốc khá ổn
      Bạn cũng có thể dùng thứ như Arduino để tạo một thiết bị USB đơn giản trao đổi thông tin với PC. Ví dụ: https://m.youtube.com/watch?v=yTc2GLXfCOY
      Sau đó, chỉ cần hiểu subsystem mà bạn quan tâm làm gì, vận hành nó ra sao, rồi viết code. Thiết bị lưu trữ, thiết bị đồ họa, v.v. thuộc nhóm này
      Raspberry Pi cũng có thể là điểm khởi đầu tốt cho các thử nghiệm như vậy. Ví dụ: Writing a bare metal operating system for the raspberry pi https://github.com/babbleberry/rpi4-osdev
    • Tutorial này thật sự rất hay
      https://wiki.osdev.org/Bare_Bones
  • Tôi thích concept của SerenityOS và trình duyệt Ladybird, nên rất vui khi thấy tiến triển như thế này

    • Đáng tiếc là giờ hai dự án đã tách nhau
      Ladybird không chỉ trở thành một dự án độc lập mà còn không còn xem SerenityOS là nền tảng mục tiêu nữa
      Ladybird đang dần loại bỏ lớp Serenity riêng của mình và thay bằng các lựa chọn chính thống hơn
      Với tư cách một người chủ yếu dùng Linux, tôi rất mong Ladybird trở thành một lựa chọn thay thế thực sự trên Linux
      Nhưng với tư cách fan SerenityOS, tôi tiếc khi năng lượng và đổi mới từng đổ vào Ladybird đang rời khỏi SerenityOS
  • Nếu cần trợ giúp về hack Chromebook, hãy hỏi trên mailing list chromium-os-dev
    Có lẽ sẽ có người giúp làm cho CCD hoạt động
    https://groups.google.com/a/chromium.org/g/chromium-os-dev?p...

  • Bootloader Depthcharge cũng hỗ trợ boot qua mạng bằng TFTP
    Bạn phải tự build và flash vào SPI, nhưng đây là tính năng rất tốt khi phát triển kernel lặp đi lặp lại
    https://chromium.googlesource.com/chromiumos/platform/depthc...

  • Tôi cứ tưởng SerenityOS đã chạy trên phần cứng thật rồi, chẳng lẽ đến giờ vẫn chỉ chạy hoàn toàn trong QEMU sao?

    • Đúng là trước đây nó từng chạy trên phần cứng thật
      Nhưng gần như không có driver đáng kể nào, nên nó chỉ hoạt động theo nghĩa cơ bản nhất, chỉ trên một số phần cứng cụ thể, và có lẽ cũng không chạy tốt lắm
      Nỗ lực lần này là để làm cho nó hoạt động đáng tin cậy trên ít nhất một nền tảng phần cứng thật
  • Serenity vẫn luôn gây ấn tượng, dù có những lúc tôi không đồng ý với cách triển khai của họ

  • Tôi đến đây để xem những thứ đáng sợ như thế này
    doas dd seek=$((0x$1)) bs=1 count=1 of=/dev/port < <(xxd -p -r <<< "$2")

    • Với người bình thường, tôi thật sự tò mò đoạn code đó nghĩa là gì