1 điểm bởi GN⁺ 2024-07-11 | 1 bình luận | Chia sẻ qua WhatsApp
  • Trong quá trình viết lại trình giả lập CPU dùng cho Time Travel Debugging bằng C++, x86/amd64 đòi hỏi cùng một hành vi cũng phải được xử lý khác nhau tùy theo cách mã hóa, prefix và chế độ thực thi
  • Có nhiều cách biểu diễn thay thế ảnh hưởng đến hiệu năng và gỡ lỗi, như mã hóa một byte CC của int 3, dạng ngắn của ADD EAX, imm, hay REX prefix không có tác dụng
  • Các lệnh INC/DEC, CMPXCHG8B/CMPXCHG16B, shift/rotate có cách xử lý flag khác với trực giác, nên dễ dẫn đến lỗi trong trình giả lập
  • Shift count không được áp dụng nguyên theo kích thước toán hạng mà bị mask, nên shr eax,20h không làm thanh ghi 32-bit thành 0 mà giữ nguyên giá trị
  • Segment vẫn được dùng để truy cập TEB trên Windows 32-bit và 64-bit; sự khác biệt về ý nghĩa của FS/GS và cách xác định base ảnh hưởng trực tiếp đến việc triển khai disassembler và trình giả lập

Các quy tắc chi tiết của x86 lộ ra khi viết lại trình giả lập TTD

  • Một thành phần của Time Travel Debuggingtrình giả lập CPU ghi lại toàn bộ quá trình thực thi của tiến trình ở mức lệnh
  • Trình giả lập của phiên bản đầu tiên, iDNA, hầu như được viết hoàn toàn bằng assembly nên nhanh, nhưng khó bảo trì và mở rộng
  • Ở phiên bản thứ hai, phần giả lập và sau đó là hầu hết các phần khác được viết lại bằng C++, với mục tiêu có một codebase dễ quản lý hơn trong khi vẫn giữ phần lớn hiệu năng của phiên bản assembly
  • Để tạo trình giả lập CPU, cần khớp đầy đủ mọi chi tiết trong hành vi của CPU; ngay cả các quy tắc quen thuộc với người có kinh nghiệm x86 cũng phải được kiểm chứng lại khi triển khai thực tế

Mã hóa x86 biểu diễn cùng một lệnh theo nhiều cách

  • x86 có thể biểu diễn cùng một lệnh bằng nhiều chuỗi byte khác nhau
  • int 3 cũng được mã hóa thành CD 03, nhưng cũng có thể mã hóa bằng CC một byte
    • Vì được dùng làm software breakpoint, cách này cho phép đặt breakpoint ngay cả tại vị trí lệnh ở cuối một trang bộ nhớ mà trang kế tiếp chưa được map
  • Cũng có các mã hóa thay thế để làm ngắn những trường hợp phổ biến
    • add eax, imm có thể được biểu diễn ngắn gọn như 05cccccccc
    • Nếu cộng cùng giá trị đó vào ECX, cần thêm 1 byte như 81c1cccccccc
  • Việc EAX được gọi là “Accumulator register” không chỉ là quy ước mà còn tạo ra khác biệt thực sự trong mã hóa; lệnh ngắn có thể có lợi cho hiệu năng vì giảm lượng dữ liệu phải chuyển từ bộ nhớ chính và giảm mức sử dụng instruction cache
  • Compiler có thể tận dụng các mã hóa ngắn này khi có thể

Prefix và giới hạn độ dài lệnh 15 byte

  • Lệnh x86 có thể có các byte prefix làm thay đổi hành vi
  • REX prefix thường dùng trong mã 64-bit được dùng để truy cập phạm vi thanh ghi rộng hơn so với mã 32-bit
  • CPU cũng chấp nhận REX prefix không có tác dụng
    • 4004cc là dạng có một byte REX đứng trước add al,0CCh 8-bit, nhưng trong trường hợp này REX không có tác dụng gì
    • Ngay cả khi gắn hai REX prefix, CPU vẫn có thể thực thi; nhiều disassembler, gồm cả WinDbg, có thể bị nhầm lẫn bởi điều này
  • Trên CPU tương thích x86, độ dài lệnh hiện tại có hard limit là 15 byte
    • Lệnh vượt quá 15 byte bị xem là lệnh không hợp lệ và gây exception
    • Trên các CPU cũ còn có những giới hạn khác với prefix, và một số prefix như LOCK có điều kiện sử dụng nghiêm ngặt hơn

Cách diễn giải thay đổi theo kích thước địa chỉ và chế độ

  • Address override prefix có thể khiến chế độ 64-bit tham chiếu địa chỉ 32-bit
    • 488d0424lea rax,[rsp]
    • 67488d0424lea rax,[esp] do prefix 0x67
  • Trong mã 32-bit, Address override đổi chế độ địa chỉ sang địa chỉ 16-bit
  • Ngay cả cùng một chuỗi byte cũng cần biết kích thước toán hạng và kích thước địa chỉ mặc định của code segment thì mới disassemble hoặc diễn giải đúng
    • 8b0424 ở chế độ 32-bit là mov eax,dword ptr [esp]
    • 8b0424 ở chế độ 64-bit là mov eax,dword ptr [rsp]
  • Dải 40~4F từng dùng cho INC regDEC reg trong x86 được dùng làm REX prefix bytes trong x64
    • Ở chế độ 32-bit, 48 03 04 24 được diễn giải thành hai lệnh: dec eaxadd eax,dword ptr [esp]
    • Ở chế độ 64-bit, 48030424 được diễn giải thành một lệnh add rax,qword ptr [rsp]
  • Các nhà thiết kế AMD64 đã dùng không gian mã hóa rộng của INC/DEC cho prefix mới để mở rộng tập thanh ghi trong chế độ 64-bit; các lệnh đó vốn đã có mã hóa khác hỗ trợ cả thanh ghi và bộ nhớ

Cạm bẫy của INC reg trong chế độ 64-bit với WinDbg

  • Vì WinDbg luôn assemble lệnh như ở chế độ 32-bit, nếu cố assemble INC reg trong mã 64-bit thì có thể cho kết quả khác ý định
  • Trong ví dụ, inc eax không trở thành lệnh tăng thật sự mà bị biến thành REX prefix vô dụng bổ nghĩa cho lệnh kế tiếp
  • Kết quả là chuỗi byte đó không được diễn giải là inc, mà là prefix đứng trước lệnh jmp

Các ngoại lệ trong hành vi của flag

  • INC EAX trông giống ADD EAX, 1, nhưng không hoàn toàn giống
    • ADD cập nhật carry flag
    • INC không cập nhật carry flag
  • Trong quá trình triển khai trình giả lập TTD, khác biệt này ban đầu đã bị triển khai sai và được phát hiện bằng unit test
  • Hầu hết các phép toán số học và logic thiết lập overflow, sign, zero, auxiliary carry, parity và carry flag
  • CMPXCHG cũng thiết lập các flag này, nhưng CMPXCHG8BCMPXCHG16B chỉ sửa zero flag
  • Một số lệnh để lại một phần flag ở trạng thái không xác định
    • Các lệnh shift và rotate để overflow flag ở trạng thái không xác định nếu shift amount lớn hơn 1
    • Hành vi thực tế của flag không xác định liên quan đến cách triển khai nội bộ của phép shift và có thể khác nhau giữa các kiến trúc
    • Có câu chuyện rằng CPU dòng Atom thực hiện bit shift trong ALU theo cách rẻ hơn và chậm hơn, khiến giá trị flag không xác định khác đi, nhưng chưa được kiểm thử trực tiếp

Mask count của lệnh shift

  • 66c1e810shr ax,10h, shift AX sang phải 16 bit
    • AX là thanh ghi 16-bit, kết quả là 0
  • c1e820shr eax,20h, nhìn bề ngoài có vẻ như là lệnh shift EAX sang phải 32 bit
  • Trên thực tế, giá trị EAX không thay đổi
    • Theo Intel SDM, count được mask với 1Fh, chỉ dùng 5 bit thấp của rotation
    • Nếu dùng prefix REX.W, mask trở thành 3Fh, nên giá trị shift tối đa là 63 bit
  • Hành vi này từng thực sự trở thành vấn đề trong một câu hỏi phỏng vấn Microsoft về “mọi cách clear thanh ghi 32-bit bằng một lệnh”
    • Người phỏng vấn nghĩ có thể dùng shift, nhưng câu trả lời là không thể với thanh ghi 32-bit

Segment vẫn tồn tại trong mã 32-bit và 64-bit

  • Segment memory có thể trông như di sản của mã 16-bit, nhưng vẫn có hiệu lực thực tế trong mã 32-bit và 64-bit
  • Hầu hết OS dùng mô hình bộ nhớ gần như flat và đặt segment base address là 0, nên bình thường ta ít để ý
    • Ở chế độ 64-bit, CPU luôn coi segment base của CS, DS, ES, SS là 0
  • Ngoại lệ là thread local storage dùng extra segment register như FS hoặc GS
  • Theo phần đính chính, base của segment FS/GS có thể được đọc ngay cả từ mã không có đặc quyền bằng các lệnh rdfsbase, wrfsbase, rdgsbase, wrgsbase
    • Các lệnh này có từ Ivy Bridge, tức từ năm 2012

Truy cập TEB của Windows và FS/GS

  • Trên Windows, FSGS được dùng để tham chiếu TEB (Thread Execution Block)
  • Cấu trúc TEB có một con trỏ self trỏ đến địa chỉ flat ở điểm bắt đầu cấu trúc, và địa chỉ này cũng chính là base của segment tương ứng
  • Trong tiến trình 32-bit, TEB nằm ở FS
    • GetLastError lấy TEB.NtTib.Self từ fs:[00000018h], rồi đọc LastErrorValue từ [eax+34h]
  • Trong tiến trình 64-bit, TEB nằm ở GS
    • GetLastError đọc con trỏ từ gs:[30h], rồi lấy giá trị từ [rax+68h]
  • Tiến trình 32-bit chạy trên OS 64-bit có cả TEB 32-bit và TEB 64-bit; có những ngữ cảnh hữu ích cần truy cập cả hai TEB, chẳng hạn mã WOW 64-bit chạy bên trong tiến trình 32-bit

Cách xác định segment base cũng khác nhau theo chế độ

  • Thiết lập CPU để xác định base address của FSGS khác nhau giữa chế độ 32-bit và 64-bit
  • Ở chế độ 32-bit, giá trị thực tế của segment register tham chiếu segment descriptor được định nghĩa trong Global Descriptor TableLocal Descriptor Table
  • Ở chế độ 64-bit, base được điều khiển bởi hai MSR
    • FS Base, IA32_FS_BASE trong Intel SDM
    • GS Base, IA32_GS_BASE trong Intel SDM
  • Do cấu trúc này, trong chế độ 64-bit, bản thân giá trị thanh ghi thực tế của FSGS không quan trọng
    • Điều quan trọng là segment override prefix
  • Khi debug tiến trình 32-bit trong WinDbg, có thể dùng giá trị thanh ghi FS để dump nội dung “FS segment”
  • Với tiến trình 64-bit, cách tương tự không hoạt động; segment override prefix có ý nghĩa hơn giá trị segment

Bài học thực tiễn cho người triển khai trình giả lập

  • Khi xây dựng trình giả lập x86, cần xử lý tỉ mỉ hành vi thực tế của CPU như mã hóa lệnh, prefix, flag, shift count và segment
  • Phần lớn các quy tắc này gần như không hữu ích trong việc viết code thông thường, nhưng lại là yêu cầu trực tiếp khi triển khai trình giả lập
  • Rất nhiều điều được học qua thử-sai và mentoring; Darek Mihocka, người có kinh nghiệm lâu năm về trình giả lập, cùng emulators.com cũng được giới thiệu
  • Nếu quan tâm đến tối ưu hóa x86 và hành vi cấp thấp, tài liệu trên website của Agner Fog rất hữu ích

1 bình luận

 
GN⁺ 2024-07-11
Ý kiến trên Hacker News
  • Tiện thể, BSF/BSR cũng có điểm kỳ quặc. Intel SDM nói rằng nếu đầu vào là 0 thì giá trị đích là không xác định, nhưng AMD lại ghi rõ rằng trong trường hợp đó thanh ghi đích không bị sửa đổi
    Nhưng glibc lại dùng nguyên xi hành vi không được tài liệu hóa này là ngay cả trên Intel thì thanh ghi đích cũng không bị sửa đổi [1]. Vì chuyện này mà tôi đã mất khá lâu để tìm ra nguyên nhân lỗi trong bộ chuyển đổi nhị phân của mình
    Ngoài ra, TZCNT/LZCNT là mã hóa BSF/BSR có thêm tiền tố F3, nhưng trên các bộ xử lý cũ không hỗ trợ phần mở rộng này thì tiền tố sẽ bị âm thầm bỏ qua. Vì vậy cùng một đoạn mã có thể hoạt động khác nhau tùy CPU, dù ít nhất chuyện này vẫn đã được tài liệu hóa
    Khi nói về mã hóa, mọi người hay chê các tiền tố, nhưng cá nhân tôi thấy đó chưa phải thứ tệ nhất. Nó khá nổi tiếng và cũng được tài liệu hóa ở mức nào đó. Còn có những điểm kỳ quặc tệ hơn. Ví dụ, các bit mở rộng REX/VEX/EVEX.RXB sẽ bị bỏ qua nếu không áp dụng được, nhưng với các thanh ghi mặt nạ k0-k7 thì lại gây ra #UD. Thế nhưng nếu thanh ghi được mã hóa trong ModRM.rm thì các bit mở rộng lại bị bỏ qua
    APX còn đẩy mức độ kỳ quặc lên thêm một bậc. Tiền tố REX2 có thể mã hóa các thanh ghi đa dụng r16-r31 nhưng không mã hóa được xmm16-xmm31, còn tiền tố EVEX thì có nhiều bố cục tùy opcode, và các bit mở rộng dùng cho thanh ghi cũng thay đổi tùy loại thanh ghi. Thanh ghi XMM dùng X3:B3:rm và V4:X3:idx, còn thanh ghi đa dụng dùng B4:B3:rm và X4:X3:idx. Đã 1 năm trôi qua mà tôi vẫn chưa xong bộ giải mã APX nên chưa thể đưa ra danh sách đầy đủ
    [1]: https://sourceware.org/bugzilla/show_bug.cgi?id=31748

    • Suốt 1 năm qua tôi đã tranh thủ từng chút thời gian để viết lại bộ giải mã x86 của QEMU. Ban đầu đó là việc cần làm để thêm hỗ trợ AVX, nhưng giờ chỉ còn lại vài opcode cần viết lại, và sau đó có lẽ việc hỗ trợ APX cũng sẽ không quá khó
      Với EVEX, tôi định giữ lại các bit thô cho đến khi đọc được opcode và xác định lớp EVEX. Tức là dự định giữ chúng ít nhất đến trước immediate, và có lẽ cả trước ModRM
      Bộ giải mã của tôi nhìn chung dựa trên các bảng trong manual, và mã phần lớn cũng ổn. Thụt lề không quá mức, các bước hầu hết đều được tách riêng hoặc dễ nhận diện. Vì đầu ra là mã JIT nên không nhất thiết phải cực kỳ hiệu quả, dễ đọc là được. Đây cũng không phải chỗ tiêu tốn phần lớn thời gian
      Dù vậy, có nhiều trường hợp manual sai hoặc không nói hết toàn bộ. Các bảng cũng đã nhiều năm không được cập nhật, ví dụ còn không có cả lệnh thanh ghi K. Có vẻ về sau sẽ phải làm thủ công nhiều hơn
      Phần chú thích trên cùng giải thích sơ qua bối cảnh: https://github.com/qemu/qemu/blob/59084feb256c617063e0dbe7e6...
      Như đã nói ở trên, vẫn còn vài lệnh do mã cũ xử lý, đặc biệt là BT/BTS/BTR/BTC. Tôi đã viết mã cho chúng nhưng vẫn chưa merge
    • Thời 486 và Pentium còn có một điểm kỳ quặc khác. BSWAP EAX chuyển đổi giữa little-endian và big-endian, và ngay từ đầu đã là lệnh 32-bit
      Nhưng lại có tiền tố 0x66 để chuyển giữa chế độ 16-bit và 32-bit. Nếu áp dụng nó cho BSWAP EAX thì sẽ xảy ra một chuyện kỳ quái không được định nghĩa
      Trên một số kiến trúc CPU, giống khác biệt giữa Intel và AMD, tiền tố này đơn giản bị bỏ qua; ở nơi khác thì nó thực hiện thứ mà tôi gọi là “hoán đổi bên trong”. Ví dụ, trong bốn byte lưu trong EAX thì byte thứ 1 và thứ 2 đổi chỗ cho nhau
      0x11223344 trở thành 0x11332244
    • Chỉ cần nghĩ tới việc phải làm cho toàn bộ logic này chạy chính xác trong silicon, lại còn phải nhanh nữa, đã thấy rợn người
      Ngày trước x86 là con hào của Intel, nhưng giờ trông nó giống gánh nặng như ác mộng mà họ phải mang theo hơn
    • Tổ hợp ngữ nghĩa và mã hóa của LZCNT cho cảm giác như một pha phản lưới nhà. Nó được mã hóa thành lệnh BSR có gắn tiền tố vốn bị bỏ qua trong legacy, và với đầu vào khác 0 thì giá trị trả về bằng kích thước toán hạng trừ đi giá trị trả về của phiên bản legacy
      Dù đúng là có hàm clz(), nhưng nếu LZCNT đơn giản chỉ là BSR với khác biệt ở ngữ nghĩa đầu vào 0, thì cái giá phải thêm một phép trừ trong phần cài đặt để đổi lấy tương thích có lẽ cũng không lớn
    • Tôi không rành lĩnh vực này lắm, nhưng sẽ rất thú vị nếu nối giao diện JTAG vào một CPU x86, cho nó chạy từng lệnh một và ghi lại toàn bộ giá trị thanh ghi
      Rồi cũng chạy cùng chương trình đó trên CPU được mô phỏng và kiểm tra ở mỗi lệnh xem trạng thái có giống nhau không, để thử xem trình giả lập có mô phỏng phần cứng một cách hoàn hảo hay không
  • Đúng là người ngầu. Viết assembly mang lại cảm giác giản dị, và tôi cũng thích cái thẩm mỹ xếp dọc của nó
    Trải nghiệm gần nhất của tôi với kiểu công việc như OP là khi cố giải thích stack cho một người bạn làm JS, rồi cả hai cùng làm một mini VM với ISA nhỏ: https://gist.github.com/darighost/2d880fe27510e0c90f75680bfe...
    Bọn tôi hoàn toàn có thể đào sâu hơn, và tôi cũng ước đã làm vậy, nhưng như thế có lẽ đã lệch khỏi mục tiêu giáo dục ban đầu. Chắc tôi nên nhắn bạn ấy xem giờ còn muốn học cùng không. Bạn tôi kiếm được khá nhiều tiền nhờ làm web ngầu, nên không có thời gian để đào sâu, còn tôi thì thất nghiệp nên thời gian và năng lượng gần như là một đại dương vô tận, thành ra cũng không dễ

    • Biết đâu nhân dịp đó bạn cũng nên học JS từ người bạn ấy
    • Là người không học chuyên ngành, đoạn mã này đã trở thành một lối vào tốt để tôi bắt đầu hiểu cách mọi thứ vận hành bên trong, và nhờ đó dẫn tôi tới một hành trình assembly khá ngầu và khó. Tôi định sẽ đào sâu thêm
  • Justine Tunney và trình giả lập của cô ấy cũng rất đáng xem. https://justine.lol/blinkenlights/
    Tài liệu ở đó cho cái nhìn tổng quan rất tuyệt về cách CPU hoạt động

    • Kinh ngạc. Lần nào cũng khiến tôi phải thán phục
    • Tôi nhớ cái tên Tunney từ thời khoảng năm 2014, khi cô ấy sống lang bạt như người vô gia cư và đăng mấy dòng nhảm trên Twitter liên quan đến Occupy
  • Có thể xem thảo luận trước đó tại đây: https://news.ycombinator.com/item?id=34636699
    Không thể tin là đã trôi qua 16 tháng rồi. Thời gian đúng là bay nhanh

  • Tôi không thật sự đồng ý mạnh với ý rằng “viết trình giả lập CPU là cách tốt nhất để thực sự hiểu CPU hoạt động thế nào”
    Cách tốt nhất là tự tạo một CPU ở mức cổng logic, như trong một môn khoa học máy tính tử tế. Việc tự làm một ARM thu gọn từ đầu thực sự rất vui

    • Tôi nghĩ cả hai đều hữu ích. Chỉ là việc thiết kế một CPU hiện đại ở mức cổng logic nằm ngoài tầm với của đa số mọi người, và có một khoảng cách rất lớn giữa CPU bạn thiết kế ở trường đại học với CPU chạy được mã thực tế
      Làm trình giả lập cho CPU hiện đại là một thử thách dễ tiếp cận hơn, và ngay cả khi chỉ chạy được một phần thì vẫn có giá trị học tập lớn
    • Đồng ý. Ngay cả một bộ xử lý cơ bản có microcode, pipeline, superscalar, dự đoán nhánh, cache dữ liệu/lệnh L1, và bộ điều khiển cache L2 write-back cũng đã không hề đơn giản
      Phần lớn kỹ sư phần mềm vẫn chưa hiểu đầy đủ về data hazard, cache invalidation và pipeline stall
    • Tôi nghĩ cả hai bên đều đúng. Việc ghép các chip 74xx lại với nhau cực kỳ thỏa mãn, và giúp bạn cảm nhận được các khía cạnh điện học cùng những đánh đổi bên trong
      Nhưng khi bắt đầu muốn làm một CPU có thể dùng cho việc gì đó có ý nghĩa, bạn sẽ dần bớt hứng thú với những chi tiết đó. Lúc ấy, độ phức tạp của hành vi và đặc tả trở nên thú vị hơn, và hướng tiếp cận bằng trình giả lập dễ xử lý hơn cũng như bao quát được nhiều kiểu hành vi hơn
    • Tôi đang đi theo Nand2Tetris và xây dần từ mức cổng logic lên, vừa mới xong chương trình giả lập VM và đúng là mất rất lâu. Giờ chuyển sang giai đoạn biên dịch
    • Ngược lại, liệu với CPU mức cổng logic bạn có thực sự triển khai đến cả phân đoạn bộ nhớ không? Tôi nghĩ để thực sự hiểu thì cần cả hai bước: trước hết làm ra một CPU hoạt động được, rồi sau đó giả lập một CPU thực tế, bao gồm cả các lỗi của nó
  • Tôi đã viết các trình giả lập nhanh cho khoảng 12 kiến trúc không phải đồ chơi, và cũng làm vài trình biên dịch JIT, nhưng x86 đến giờ vẫn gây PTSD cho tôi. Tôi chưa từng thấy kiến trúc nào bừa bộn đến thế. Nó có lịch sử và có lý do, nhưng vẫn thật quá đáng

    • Học kiến trúc x86 giống như học một ngôn ngữ có đầy bất quy tắc, cơ quan thoái hóa và các hệ ngữ pháp cạnh tranh nhau, ví dụ như tiếng Pháp. Các kiến trúc khác như RISC-V hay ARMv8 nhất quán hơn nhiều
    • Nếu nói “chưa từng thấy kiến trúc nào bừa bộn hơn thế” thì còn có Itanium. Mỗi lần mở manual ra là lại phát hiện thêm một thứ khiến bạn phải nghĩ “rốt cuộc họ đang nghĩ gì vậy?”, dù chẳng cần cố tìm
    • Đồng cảm. Có lẽ đáng ra tôi nên bắt đầu bằng cái đó ngay từ đầu
  • Gần đây tôi triển khai phần lớn của một bộ giải mã x86-64 như một side project [1], và khá ngạc nhiên khi thấy dạo này nó còn phức tạp hơn trước. Với mục đích của tôi thì Sandpile.org [2] thực sự rất hữu ích
    [1] Chính xác hơn thì đây là phiên bản x86-64 của disfilter của Fabian Giesen, được làm cho một side project khác chưa công bố: https://gist.github.com/lifthrasiir/df47509caac2f065032ef72e...
    [2] https://sandpile.org/

  • Trình disassembler 68k tôi làm ở đại học giống hệt khoảnh khắc Neo nói “Tôi biết kung fu”. Nó là mắt xích còn thiếu, giúp tôi suy luận ngược lại về mã từ ngôn ngữ bậc cao xuống transistor rồi quay trở lên
    Viết cả một trình giả lập chắc còn hiệu quả hơn cả một bậc độ lớn. Bài viết hay

    • Tôi cho rằng viết trình giả lập ISA thực ra không giúp ích nhiều để hiểu CPU superscalar hiện đại hoạt động ra sao. Gần như mọi thứ quan trọng đều là các tối ưu hóa bị ẩn bên trong
  • Có lẽ trí nhớ của tôi đã sai. Tôi nhớ các biến thể salsa20 và mã máy ban đầu nằm ở cryp.to, nhưng trang của Dan Bernstein là https://cr.yp.to/
    Hồi còn ở startup đánh giá mã hóa dữ liệu lưu trữ, mã hóa luồng, v.v., trang của Dan có nhiều bản triển khai theo từng chipset đích và tập lệnh khác nhau. Chúng được cross-compile từ cách biểu diễn assembler của ông
    Thú vị là khi dùng VM có thể thấy hồi đầu đến giữa những năm 2000 khi đó các tập lệnh nào được hỗ trợ. Trong lúc kiểm thử, đôi khi phát sinh vấn đề vì VM tuy tuyên bố hỗ trợ nhưng triển khai thực tế lại chưa đầy đủ

    • Ý bạn là trang của Dan Berstain đúng không… khoan đã
  • Thú vị là ở đây có nhiều ý kiến nói assembly x86 đau khổ đến mức nào khi so với RISC. Tôi lại gặp đúng vấn đề ngược lại khi tách mã trở lại thành các object file
    Với mục đích này thì phân tích x86 thực sự rất dễ, còn MIPS thì như ác mộng. Chủ yếu vì thứ tôi quan tâm là các tham chiếu tới mã và dữ liệu. x86 có hằng tức thời cỡ con trỏ, còn MIPS có các cặp relocation HI16/LO16, kéo theo đủ thứ rắc rối với đồ thị sử dụng thanh ghi, luồng mã và các lệnh delay branch
    Nói vậy không có nghĩa là tôi đang khen x86

    • Đúng vậy. x86 thì kỳ quặc, nhưng lệnh có độ dài biến đổi sau khi bung ra dưới dạng văn bản thì thực ra khá dễ nhìn và dễ hiểu. Vấn đề là không an toàn vì có thể giấu lệnh khác vào giữa một lệnh
      Điều lớn nhất học được khi so sánh assembly x86 với C là signed/unsigned không còn là thuộc tính của kiểu mà là thuộc tính của phép toán
      Sẽ tốt hơn nếu tận dụng được cờ, và ở vài kiến trúc như PPC hay armv7 thì dễ hơn, nhưng trên x86 thì cờ bị ghi đè quá dễ nên rất khó khai thác giá trị của chúng