Chuyển đến nội dung
MINH VO Ghi chép từ công việc
của một kỹ sư tại Việt Nam
Cơ bản10 phút đọc

Đọc và thay trường bit mà không làm mất các bit lân cận

Thay trường bit với độ rộng rõ ràng, kiểm tra các bit bên ngoài và phân tích lỗi với thanh ghi W1C hoặc cập nhật đồng thời.

Minh họa bảng mạch đặt trên giấy vẽ với các nét phác kỹ thuật mờ

Thay một trường bit cần hai thao tác: xóa các bit cũ của trường, rồi chèn giá trị mới vào đúng vị trí. Chỉ dùng OR không thể xóa một bit vốn đã bằng một. Với lập trình viên đang đọc dữ liệu đóng gói, điểm khởi đầu dễ kiểm soát nhất là một từ dữ liệu phần mềm không dấu thông thường, có độ rộng rõ ràng và giá trị trường được kiểm tra trước.

Giả sử một từ tám bit chứa trường chế độ rộng ba bit tại các vị trí từ hai đến bốn. Vị trí được đánh số từ không ở bit có trọng số nhỏ nhất. Mặt nạ chưa dịch của trường là nhị phân 111, tức bảy. Dịch mặt nạ đó sang trái hai vị trí tạo ra 00011100, tức 0x1C.

Thay một trường bằng cách tính từng bước

Lấy từ ban đầu 0xAB, có các bit 10101011. Chế độ chiếm các bit bốn, ba và hai, nên giá trị hiện tại là nhị phân 010, tức hai. Trích xuất nó bằng (word >> 2) & 7.

Để thay chế độ bằng năm, trước tiên giữ lại mọi bit ngoài trường. XOR mặt nạ trường với mặt nạ tám bit toàn một để có 11100011, tức 0xE3. AND giá trị đó với từ ban đầu. Kết quả trung gian là 10100011, tức 0xA3. Dịch năm, có dạng nhị phân 101, sang trái hai vị trí để được 00010100. OR hai phần lại tạo ra 10110111, tức 0xB7.

Ba bit trên cùng và hai bit dưới cùng không thay đổi. Chỉ các vị trí từ hai đến bốn được phép khác trước. Đây là một bất biến có thể kiểm tra riêng, bên cạnh việc chế độ mới có được mã hóa đúng hay không.

Từ tám bit xóa các vị trí từ hai đến bốn trước khi chèn chế độ năm, biến 0xAB thành 0xB7Mở ảnh kích thước đầy đủ ↗

Nếu OR trực tiếp, đầu vào này sẽ tạo ra 0xBF. Chế độ cũ có một bit một ở vị trí mà chế độ mới cần bit không, nên OR giữ lại bit đó và mã hóa thành bảy thay vì năm. Với một số giá trị ban đầu, lỗi này không xuất hiện. Vì vậy, một ví dụ chạy đúng là bằng chứng còn yếu.

Kiểm tra cả hai yêu cầu

Thao tác cập nhật trường phải bảo đảm trích xuất lại được đúng giá trị yêu cầu và mọi bit khác vẫn giữ nguyên. Miền kiểm tra đầy đủ ở đây khá nhỏ: 256 từ ban đầu nhân với tám giá trị chế độ cho 2.048 trường hợp.

WIDTH_MASK = 0xff
SHIFT = 2
FIELD_MASK = 0b111 << SHIFT
OUTSIDE_MASK = WIDTH_MASK ^ FIELD_MASK

def replace_mode(word, mode):
    if not 0 <= word <= WIDTH_MASK:
        raise ValueError("word must fit in eight bits")
    if not 0 <= mode <= 7:
        raise ValueError("mode must fit in three bits")
    return (word & OUTSIDE_MASK) | (mode << SHIFT)

for word in range(256):
    for mode in range(8):
        result = replace_mode(word, mode)
        assert ((result >> SHIFT) & 7) == mode
        assert (result & OUTSIDE_MASK) == (word & OUTSIDE_MASK)
print("2048 field replacements passed")

Từ chối chế độ nằm ngoài miền là một lựa chọn có chủ đích của giao diện hàm. Nếu thay vào đó lấy AND chế độ với bảy, giá trị sẽ bị giảm theo modulo tám. Chẳng hạn, chín sẽ âm thầm trở thành một. Điều này có thể phù hợp khi giao thức quy định rõ phải cắt bớt bit, nhưng thường không hữu ích nếu bên gọi tin rằng mình đã truyền một giá trị liệt kê hợp lệ.

Kiểm tra từ đầu vào cũng ngăn số âm vô tình mang theo cách xử lý bit có dấu không giới hạn của Python. Trong C, hãy dùng toán hạng không dấu và bảo đảm số vị trí dịch nhỏ hơn độ rộng của toán hạng sau phép nâng kiểu nguyên. Một biểu thức có vẻ chạy được với một trình biên dịch vẫn có thể vi phạm quy tắc ngôn ngữ nếu phép dịch hoặc giá trị trung gian có dấu không hợp lệ. Mục 6.5.7 của bản dự thảo ủy ban C11 N1570 quy định các yêu cầu đối với toán hạng sau nâng kiểu và số vị trí dịch.

Bố trí logic và thứ tự byte là hai vấn đề riêng

Định nghĩa trường ở trên nói về trọng số bit bên trong một số nguyên. Thứ tự byte, hay endianness, quyết định cách một số nguyên nhiều byte ánh xạ đến các địa chỉ byte. Trường tại vị trí bit 12 không trở thành một trường toán học khác khi cùng số nguyên đó được lưu theo little-endian; điều thay đổi là byte chứa nó trong bộ nhớ.

Với gói tin mạng, hãy giải mã các byte theo thứ tự byte mà giao thức quy định trước khi dùng mặt nạ số nguyên, hoặc truy cập trực tiếp các byte đã được chỉ định. Đừng giả định một cấu trúc trường bit của C có đúng bố trí trên đường truyền mà bạn cần. Thứ tự phân bổ, phần đệm và chi tiết biểu diễn có thể phụ thuộc triển khai và ABI. Dùng mặt nạ tường minh và ghép byte rõ ràng giúp người đọc kiểm tra được quy ước.

Các định nghĩa lệnh nạp, lưu và logic của RV32I cho thấy những thao tác mức thấp mà trình biên dịch có thể dùng để thực hiện công việc này. Chúng mô tả độ rộng thanh ghi và truy cập bộ nhớ, còn đặc tả gói tin quy định bố trí trường thực tế. Hai nguồn này không thể thay thế nhau.

Tổng quát hóa trường mà vẫn phát hiện đầu vào sai

Với từ rộng N bit, trường rộng W bit bắt đầu tại vị trí dịch S phải thỏa 1 <= W <= N, 0 <= SS + W <= N. Đây là những yêu cầu về cấu trúc. Nếu trường vượt khỏi ranh giới từ, âm thầm cắt bớt kết quả cuối sẽ làm thay đổi ý nghĩa của giao diện, chứ không sửa được đầu vào sai.

Mặt nạ trường chưa dịch có W bit thấp bằng một. Dịch nó sang trái S vị trí, xóa vùng tương ứng trong từ, rồi chèn giá trị đã được giới hạn từ không đến 2**W - 1. Hàm Python dùng số nguyên độ chính xác tùy ý dưới đây kiểm tra các điều kiện trước khi thực hiện thao tác.

def replace_field(word, value, total_bits, shift, width):
    if total_bits < 1 or width < 1 or shift < 0 or shift + width > total_bits:
        raise ValueError("field must lie inside the word")
    all_bits = (1 << total_bits) - 1
    value_mask = (1 << width) - 1
    if not 0 <= word <= all_bits or not 0 <= value <= value_mask:
        raise ValueError("word or field value is out of range")
    field_mask = value_mask << shift
    return (word & (all_bits ^ field_mask)) | (value << shift)

assert replace_field(0xabcd, 0x12, 16, 4, 8) == 0xa12d
assert replace_field(0xffff, 0, 16, 0, 16) == 0

Ví dụ đầu đi qua ranh giới giữa hai byte. Các bit từ bốn đến mười một của 0xABCD chứa 0xBC. Thay chúng bằng 0x12 tạo ra 0xA12D, giữ nguyên nhóm bốn bit cao A và nhóm bốn bit thấp D. Nếu từ được mã hóa thành các byte big-endian, AB CD trở thành A1 2D. Phép tính trường logic được thực hiện trước lựa chọn tuần tự hóa đó.

Python có thể tính 1 << 16 mà không thu hẹp kết quả. Trong C, biểu thức như 1u << 32 không phải cách khả chuyển để tạo mặt nạ đủ 32 bit khi unsigned int rộng 32 bit: dịch một lượng bằng độ rộng toán hạng vi phạm quy tắc phép dịch của ngôn ngữ. Hàm C làm việc với độ rộng cố định cần xử lý riêng trường hợp phủ toàn bộ từ, hoặc dùng cách tạo mặt nạ mà mọi phép dịch đều nằm trong miền hợp lệ. Hãy kiểm tra kiểu trung gian, không chỉ kiểu được khai báo cho biến kết quả.

Các ca kiểm tra hữu ích gồm trường một bit tại mỗi đầu, trường chiếm toàn bộ từ, giá trị hợp lệ lớn nhất, số không và từng ranh giới bị từ chối. Bất biến giữ nguyên bit ngoài trường phải đúng cho mọi trường hợp được chấp nhận. Bộ kiểm thử chỉ dùng trường nằm sâu bên trong từ có thể bỏ sót chính trường hợp dịch bằng độ rộng làm hỏng một hàm tổng quát.

Phân tích thanh ghi có nhiều quy tắc ghi

Xét một thanh ghi thiết bị tám bit giả định. Các bit từ không đến ba là cờ sự kiện được xóa bằng cách ghi một, thường gọi là write-one-to-clear hoặc W1C. Các bit từ bốn đến sáu là chế độ đọc/ghi thông thường. Bit bảy được dành riêng và bắt buộc phải ghi bằng không. Các quy tắc này được đặt ra cho ví dụ, không phải đặc tả của một bo mạch cụ thể.

Giả sử đọc thanh ghi được 0x25. Chế độ hiện tại là hai, còn các cờ sự kiện không và hai đang bật. Cập nhật đọc-sửa-ghi thông thường để đổi chế độ sang năm sẽ tạo 0x55: nó giữ các bit trạng thái thấp và thay các bit chế độ. Nhưng khi ghi 0x55, cả hai sự kiện đã quan sát đều bị xóa vì các vị trí W1C nhận giá trị một.

Theo quy tắc giả định này, giá trị ghi chỉ để đổi chế độ phải là 0x50. Trường chế độ của nó chứa năm, trường trạng thái chứa các số không nên không xóa cờ, và bit dành riêng bằng không. Giữ nguyên các bit cũ bằng phép toán số học lại là thao tác sai đối với những vị trí trạng thái này.

Đọc thanh ghi hỗn hợp giả định được 0x25; đọc-sửa-ghi thông thường tạo giá trị có hại 0x55, còn ghi riêng chế độ bằng 0x50 giữ nguyên các cờ W1CMở ảnh kích thước đầy đủ ↗

Tài liệu thanh ghi phải giải thích ghi không và ghi một có tác dụng gì ở từng trường. Thiết bị có thể cung cấp địa chỉ riêng để đặt hoặc xóa bit, yêu cầu độ rộng truy cập cụ thể, hoặc cấm ghi một số bit. Hàm hỗ trợ phần mềm không thể suy ra các quy tắc này chỉ từ vị trí bit. Tài liệu truy cập thiết bị của Linux là cơ sở cho yêu cầu dùng hàm truy cập MMIO và thứ tự thao tác phù hợp, còn tài liệu của ngoại vi mới quy định ngữ nghĩa ghi từng thanh ghi.

Giữ được hai cập nhật đồng thời

Giả sử hai luồng cập nhật các trường không chồng lấn trong một byte phần mềm dùng chung thông thường. Cả hai cùng đọc số không. Luồng A đổi trường thấp thành ba, tạo 0x03. Luồng B đổi trường cao thành năm, tạo 0x50. Nếu B ghi sau cùng, từ cuối bằng 0x50 và cập nhật của A bị mất, dù phép tính mặt nạ cục bộ của cả hai luồng đều giữ nguyên các bit ngoài trường trong giá trị mà chúng từng đọc.

Nếu ứng dụng muốn giữ cả hai cập nhật, kết quả kết hợp phải là 0x53. Có thể dùng mutex để bảo vệ toàn bộ thao tác đọc-sửa-ghi. Một vòng lặp so sánh-và-trao đổi cũng có thể thử lại từ giá trị mới quan sát được, nếu ngôn ngữ và đối tượng lưu trữ hỗ trợ thao tác nguyên tử. Khi thử lại, phải tính lại từ mong muốn dựa trên giá trị vừa đọc; liên tục thử ghi lại 0x50 ban đầu sẽ không kết hợp được cập nhật của A.

Các cơ chế đó có yêu cầu riêng. Một cập nhật nguyên tử với thứ tự bộ nhớ relaxed có thể bảo vệ phép cập nhật từ đóng gói nhưng không thiết lập thứ tự công bố cho dữ liệu khác. Một triển khai không dùng khóa vẫn có thể tranh chấp, hoặc có luồng liên tục không tiến được nếu các giả định không phù hợp. Áp dụng thao tác so sánh-và-trao đổi thông thường của CPU lên MMIO không thay thế cho giao thức thiết bị được tài liệu hóa và có thể không được hỗ trợ.

Từ đây ta có hai phép kiểm tra lỗi riêng. Ví dụ W1C hỏi liệu ghi lại các bit được giữ nguyên có làm thay đổi trạng thái thiết bị hay không. Ví dụ hai luồng hỏi liệu một cập nhật khác có thể xảy ra giữa lần đọc và lần ghi hay không. Kiểm tra mặt nạ trường thành công không trả lời được câu nào trong hai câu đó. Vì vậy, khi giá trị đóng gói được dùng qua giao diện thiết bị hoặc bộ nhớ dùng chung, hãy ghi nhận đủ cả ba lớp: phép tính bit, tác dụng của truy cập và sự phối hợp giữa các bên cập nhật.

Nguồn tham khảo & đọc thêm

  1. RISC-V unprivileged ISA: RV32I base integer instruction set
  2. Linux kernel: Bus-independent device accesses
  3. ISO C11 committee draft N1570: integer ranges, expressions and fixed-width types
← Về trang bài viết

Hình minh họa

100%