Khi lệnh ldaxr không hoạt động: Truy cập độc quyền và khả năng lưu cache trên AArch64

Công nghệ10 tháng 10, 2026·7 phút đọc

Bài viết kể lại hành trình gỡ lỗi của tác giả khi phát triển hệ điều hành floss cho kiến trúc AArch64, tập trung vào việc một spin lock chạy tốt trên QEMU nhưng gây ngoại lệ trên Raspberry Pi 5 thật. Nguyên nhân nằm ở việc lệnh load exclusive (ldaxr) yêu cầu khả năng lưu cache phải được bật, cả ở cấp toàn cục (SCTLR.C) lẫn cấp thuộc tính bộ nhớ Write-Back.

Trong giới lập trình hệ thống, có những lỗi chỉ xuất hiện khi bạn rời khỏi môi trường mô phỏng và chạy thử trên phần cứng thật. Đó chính là câu chuyện mà tác giả Joel Siks chia sẻ khi phát triển floss — một hệ điều hành (thực chất là nhân kernel) viết từ đầu cho kiến trúc AArch64.

Sau khi hoàn thành bài viết trước về ngắt và bộ điều khiển ngắt chung trên AArch64, tác giả bắt tay vào triển khai spin lock để đồng bộ hóa giữa các nhân CPU. Mọi thứ chạy trơn tru trên QEMU, nhưng khi đưa lên Raspberry Pi 5 (rpi5), một ngoại lệ bí ẩn xuất hiện đúng tại lệnh ldaxr. Bài viết dẫn dắt người đọc qua hành trình tìm ra nguyên nhân gốc: khả năng lưu cache (cacheability).

Vấn đề: Spin lock chạy tốt trên QEMU nhưng lỗi trên phần cứng thật

Khi kích hoạt các nhân phụ, mỗi nhân in ra ID của mình trong quá trình khởi động, ví dụ "Running core 0". Không có cơ chế loại trừ tương hỗ, các nhân sẽ tranh nhau và kết quả in ra bị xen kẽ, lộn xộn:

Running core 0
RRRununing cournnien ng 2cni
onrge  1co
re 3

Để có đầu ra rõ ràng, tác giả viết một spin lock bằng assembly thủ công, sử dụng cặp lệnh load exclusive (ldaxr) và store exclusive (stxr). Nguyên lý khá đơn giản:

  • ldaxr đọc giá trị khóa với ngữ nghĩa acquire, đảm bảo không có truy cập bộ nhớ nào phía sau bị đảo lên trước nó.
  • stxr chỉ thành công nếu không có ghi nào khác vào cùng địa chỉ đó kể từ lệnh ldaxr.
  • Lệnh mở khóa dùng stlr với ngữ nghĩa release.

Trên các bo mạch virt và rpi4 của QEMU, spin lock hoạt động hoàn hảo. Tác giả chạy lại chương trình vài nghìn lần mà không thấy đầu ra bị lộn xộn.

Nhưng khi chạy trên Raspberry Pi 5 thật, xuất hiện ngoại lệ Data Abort với mã Exception Class (EC) là 37 (0x25). Khi phân tích địa chỉ 0x84474, có thể thấy CPU đang thực thi đúng lệnh ldaxr:

84474:       885ffc20        ldaxr   w0, [x1] <-- CPU dừng ở đây
84478:       35ffffe0        cbnz    w0, 84474

Điều thú vị: nếu thay ldaxr/stxr bằng lệnh load acquire thường (ldar) và store thường (str), lỗi biến mất. Điều này cho thấy có gì đó đang chặn các thao tác độc quyền (exclusive).

Truy tìm nguyên nhân: Kiểu bộ nhớ và thuộc tính bộ nhớ

Theo tài liệu Implementation Software Synchronization Primitives in A64 của Arm, các lệnh nguyên tử chỉ được đảm bảo tính nguyên tử về mặt kiến trúc khi bộ nhớ có thuộc tính:

  • Inner Shareable / Outer Shareable
  • Write-Back cho cả Inner và Outer
  • Có gợi ý Read allocation và Write allocation, và không phải transient

Ngược lại, bộ nhớ Device, bộ nhớ Non-cacheable, hoặc bộ nhớ bị đối xử như Non-cacheable, có thể không hỗ trợ lệnh nguyên tử.

Trong floss, tác giả cấu hình hai tập thuộc tính bộ nhớ thông qua thanh ghi MAIR_EL1:

  • Device: 0b00000000 — bộ nhớ Device-nGnRnE
  • Normal: 0b11111111 — Normal, Outer/Inner Write-Back, có gợi ý allocation, non-transient

Nhìn qua thì cấu hình Normal đã hoàn toàn thỏa mãn yêu cầu kiến trúc cho lệnh nguyên tử. Vậy tại sao vẫn lỗi?

Phát hiện then chốt: Bit C trong thanh ghi SCTLR

Câu trả lời nằm ở một cài đặt toàn cục trong thanh ghi điều khiển hệ thống SCTLR: bit "C", điều khiển khả năng cache cho các truy cập dữ liệu ở EL1/EL0.

Trên Raspberry Pi 5, bit này mặc định bằng 0, tức là data cache bị tắt cho tất cả các nhân. Đặt bit C = 1 khiến ngoại lệ biến mất hoàn toàn.

Tài liệu về hệ thống bộ nhớ L1 trên CPU Cortex-A76 (CPU của rpi5) giải thích rõ: khi data cache bị tắt, mọi lệnh load/store tới bộ nhớ cacheable đều bị đối xử như Non-cacheable và không nhất quán (incoherent) với cache của chính nhân đó lẫn các nhân khác.

Tác giả còn thực hiện một thử nghiệm nhỏ với các cấu hình MAIR khác nhau:

Mẫu bitMô tảCó ngoại lệ?
0b00110011Normal, Outer/Inner Write-Through TransientCó
0b01000100Normal, Outer/Inner Non-cacheableCó
0b01110111Normal, Outer/Inner Write-Back TransientKhông
0b10111011Normal, Outer/Inner Write-Through Non-transientCó
0b11111111Normal, Outer/Inner Write-Back Non-transientKhông

Kết quả cho thấy lệnh load exclusive không hoạt động nếu thiếu thuộc tính Write-Back. Điều này khớp với tài liệu của Cortex-A76: "một trang chỉ được coi là cacheable nếu cả thuộc tính Inner và Outer đều là Write-Back. Trong mọi trường hợp khác, các trang đều bị hạ cấp thành Non-cacheable Normal".

Nghĩa là dù Write-Through về mặt lý thuyết vẫn "ghi vào cache", CPU Cortex-A76 thực tế không cache loại bộ nhớ đó.

Tại sao QEMU không phát hiện ra?

Điều đáng thất vọng là QEMU không bắt được lỗi này. QEMU chưa hỗ trợ bo mạch rpi5, và khi đào vào mã nguồn QEMU, tác giả nhận ra rằng khả năng cache đơn giản là không được mô phỏng chút nào. Hằng số cho bit SCTLR_C thậm chí không được tham chiếu ở bất kỳ đâu.

Đây là một bài học quan trọng cho các nhà phát triển hệ thống: đừng giả định mọi thứ hoàn hảo chỉ vì nó chạy tốt trên trình mô phỏng.

Exclusive Monitor: Mảnh ghép cuối cùng

Để đạt tính nguyên tử, các lệnh load/store exclusive sử dụng các exclusive monitor. Mỗi nhân có một local monitor riêng, và tất cả các nhân chia sẻ một global monitor. Mỗi monitor có hai trạng thái: open và exclusive.

Các truy cập độc quyền tới vùng nhớ Non-shareable chỉ tác động lên local monitor, còn vùng nhớ shareable (Inner hoặc Outer) sẽ tác động lên cả local lẫn global monitor.

Điểm mấu chốt nằm ở đây — theo tài liệu của Arm:

"Mặc dù chúng ta mô tả local và global monitor như những thứ khác nhau, chúng thực tế có thể chia sẻ logic. Ví dụ, một cách triển khai là dùng kết hợp các local monitor theo từng nhân với logic nhất quán cache để cung cấp chức năng global monitor cho các vị trí cacheable."

Có vẻ như CPU Cortex-A76 của rpi5 triển khai global monitor thông qua logic nhất quán cache. Do đó, bất kỳ thao tác bộ nhớ độc quyền nào tới vùng nhớ được đánh dấu shareable (vốn cần truy cập global monitor) sẽ thất bại khi khả năng cache bị tắt.

Kết luận

Từ cuộc phiêu lưu gỡ lỗi này, tác giả rút ra kết luận: trên cấu hình Raspberry Pi 5 với CPU Arm Cortex-A76, một thao tác load exclusive trên bộ nhớ shareable (giả định truy cập global exclusive monitor) không hoạt động trừ khi khả năng cache được bật ở cả hai cấp:

  • Toàn cục: SCTLR.C = 1
  • Cục bộ: thuộc tính bộ nhớ của entry bảng dịch địa chỉ phải có Write-Back

Trong quá trình phát triển, tác giả thường xuyên lặp nhanh với QEMU nhưng cũng định kỳ kiểm tra trên phần cứng thật. Điều này tạo ra nhiều "tầng" gây bối rối: QEMU nói một đằng, tài liệu Arm ARM nói một nẻo, và cách triển khai Cortex-A76 lại nói một kiểu khác.

Đây là lỗi thú vị nhất mà tác giả gặp phải trong quá trình phát triển floss — nhưng chắc chắn không phải lỗi cuối cùng.

Bài học dành cho lập trình viên hệ thống và những ai quan tâm đến kiến trúc Arm: mô phỏng chỉ là điểm khởi đầu, và những chi tiết như khả năng cache, thuộc tính bộ nhớ hay exclusive monitor có thể tạo ra khác biệt lớn giữa lý thuyết và thực tế.

Chia sẻ:FacebookX
Nội dung tổng hợp bằng AI, mang tính tham khảo. Xem bài gốc ↗