rr cho C/C++ – trình gỡ lỗi ghi lại và phát lại
(rr-project.org)- rr là công cụ giúp ghi lại một lần việc thực thi bị lỗi trên Linux rồi phát lại cùng lần thực thi đó nhiều lần bằng gdb, từ đó dễ theo vết các lỗi C/C++ khó tái hiện
- Trong lúc phát lại, không gian địa chỉ, thanh ghi, dữ liệu lời gọi hệ thống và bố cục bộ nhớ luôn giống hệt mỗi lần, nên các manh mối gỡ lỗi như địa chỉ đối tượng hay thứ tự sự kiện không bị mất đi
- Hỗ trợ các lệnh gdb thông thường, scripting và tích hợp IDE, đồng thời kết hợp data watchpoint phần cứng với thực thi ngược để quay lại đúng điểm giá trị bị thay đổi
- Có thể xử lý Firefox, Chrome, QEMU, LibreOffice, chương trình Go, workload đa tiến trình và cả container, nhưng vẫn có giới hạn về giả lập lõi đơn, bộ nhớ dùng chung và phạm vi hỗ trợ CPU·lời gọi hệ thống
- Có thể lưu lại các lỗi gián đoạn hoặc lỗi do fuzzer/chèn lỗi ngẫu nhiên tạo ra để phân tích lặp lại, giúp giảm chi phí sửa lỗi và nâng cao chất lượng phần mềm
Mô hình gỡ lỗi của rr
- rr là công cụ gỡ lỗi C/C++ cho Linux, không nhằm thay thế gdb mà tăng cường quy trình làm việc với gdb bằng khả năng ghi và phát lại
- Sau khi lưu một lần thực thi có phát sinh lỗi, bạn có thể gỡ lỗi lại đúng lần thực thi đó bao nhiêu lần tùy ý
- Việc phát lại không thay đổi theo kiểu bất định như chạy trực tiếp, mà luôn tiến hành với cùng một trạng thái mỗi lần
- Cung cấp thực thi ngược hiệu quả dưới gdb
- Có thể đặt breakpoint và data watchpoint
- Có thể reverse-execute nhanh tới điểm cần quan tâm
- Được dùng trong ứng dụng thực tế, và nhiều lập trình viên đang tận dụng nó để sửa lỗi thật
Tính năng hỗ trợ và phạm vi áp dụng
- Các tính năng của rr được thiết kế theo ứng dụng thực chiến và quy trình làm việc dựa trên gdb
- Overhead thấp hơn các công cụ tương tự, đặc biệt với workload chủ yếu đơn luồng
- Hỗ trợ ghi và phát lại nhiều ứng dụng như Firefox, Chrome, QEMU, LibreOffice, chương trình Go
- Hỗ trợ ghi, phát lại và gỡ lỗi workload đa tiến trình bao gồm cả toàn bộ container
- Hỗ trợ gdb scripting và tích hợp IDE
- Hỗ trợ trace bền vững, nén được và có thể di chuyển giữa các máy
- Cung cấp Chaos mode để làm lộ rõ hơn các lỗi gián đoạn
Luồng ghi và phát lại
- Ứng dụng được ghi bằng
rr record /your/application --args - Toàn bộ lần chạy, bao gồm cả lỗi, được lưu xuống đĩa và sau đó có thể gỡ lỗi bằng
rr replay - Trong lúc phát lại, bạn không gỡ lỗi một lần chạy trực tiếp mà là trace đã được ghi lại
- Không gian địa chỉ, nội dung thanh ghi và dữ liệu lời gọi hệ thống được giữ nguyên trong mọi lần phát lại
- Có thể dùng nguyên vẹn hầu hết các lệnh gdb thông thường
- Khi cần khởi động lại phiên gỡ lỗi, dùng lệnh
runcủa gdb để phát lại bản ghi từ đầu- Sau khi khởi động lại, cùng một lần thực thi sẽ lại được phát
- Trạng thái gỡ lỗi được giữ nguyên giữa các lần khởi động lại
- Ngay cả con trỏ
thiscủa đối tượng cấp phát động cũng giống nhau giữa các phiên phát lại- Vì cấp phát bộ nhớ luôn như nhau mỗi lần, bạn có thể hardcode địa chỉ cần theo dõi
Thực thi ngược và watchpoint
- rr cho phép gỡ lỗi theo cách lần ngược từ kết quả về nguyên nhân khi tìm lý do một giá trị bị đặt sai
- Trong ví dụ gỡ lỗi layout của Firefox, sau khi xác nhận giá trị
mRect.widthbị sai, người dùng sử dụngwatch -l mRect.widthvàreverse-cont - Bằng cách kết hợp watchpoint phần cứng với reverse execution, có thể tìm ra điểm giá trị đổi từ
11220sang12000 - Cách này giúp giảm công sức quay ngược từ thời điểm lỗi biểu hiện về đúng chỗ giá trị thực sự bị thay đổi
Cài đặt và bắt đầu
- Nếu muốn build từ mã nguồn, chỉ cần làm theo hướng dẫn Building And Installing
- Khi gói cài đặt không phù hợp, nên build từ source
- Đôi khi thay đổi kernel hoặc cập nhật OS sẽ đòi hỏi rr phải thay đổi theo
- Ví dụ cài trên Fedora là tải
rr-5.9.0-Linux-$(uname -m).rpmrồi cài bằngsudo dnf install - Ví dụ cài trên Ubuntu là tải
rr-5.9.0-Linux-$(uname -m).debrồi cài bằngsudo dpkg -i
Cách xử lý lỗi gián đoạn
- Động lực ban đầu của rr là làm cho việc gỡ lỗi lỗi gián đoạn trở nên dễ dàng hơn
- Lỗi gián đoạn khó gỡ vì có lần chạy thì lỗi không xuất hiện
- Có thể ghi lại các lần chạy thử với overhead thấp, rồi khi lỗi xảy ra thì phát lại chính lần chạy đó nhiều lần trong debugger
- Phát lại có tính quyết định cũng giúp tích lũy manh mối trong việc gỡ lỗi các bug thông thường
- Với debugger thông thường, khi chạy lại thì địa chỉ đối tượng đang quan tâm hoặc thứ tự sự kiện quan trọng có thể thay đổi, khiến thông tin trước đó mất tác dụng
- Với rr, kiến thức về lần chạy thất bại vẫn được giữ nguyên dù phát lại nhiều lần
- Gỡ lỗi thường là quá trình lần ngược từ kết quả về nguyên nhân, nên nếu có thể chạy ngược theo thời gian thì việc truy nguyên nhân sẽ dễ hơn
- rr cung cấp hệ thống ghi-phát lại overhead thấp và backend thực dụng cho các lệnh reverse execution của gdb
- Đây là công cụ hoạt động thực tế, được lập trình viên dùng thường xuyên trong nhiều dự án lớn nhỏ
Trạng thái được phát lại có tính quyết định bảo đảm
- rr ghi lại một nhóm tiến trình không gian người dùng trên Linux
- Nó thu thập toàn bộ đầu vào và các hiệu ứng CPU bất định mà các tiến trình được ghi nhận từ kernel
- Việc phát lại bảo đảm giữ nguyên luồng điều khiển ở cấp lệnh máy, bộ nhớ và nội dung thanh ghi
- Bố cục bộ nhớ luôn như nhau, địa chỉ đối tượng không đổi, giá trị thanh ghi giống hệt, và các lời gọi hệ thống trả về cùng dữ liệu
- Fuzzer và các công cụ chèn lỗi ngẫu nhiên trở nên mạnh hơn khi dùng cùng rr
- Các công cụ này rất giỏi tạo ra lỗi gián đoạn nhưng có thể khó tái hiện lại đúng cùng một lỗi
- rr ghi lại lần chạy ngẫu nhiên đó và, nếu có lỗi, cho phép gỡ lỗi vấn đề một cách có tính quyết định bằng bản ghi đã lưu
Khác biệt với các công cụ ghi-phát lại truyền thống
- rr hiện thực hóa ý tưởng record-and-replay debugging đã có từ lâu, nhưng theo các mục tiêu thiết kế cụ thể
- Trọng tâm ban đầu là Firefox
- Nhiều kỹ thuật ghi-phát lại yêu cầu ngôn ngữ riêng hoặc không phù hợp về quy mô, nên khó áp dụng cho Firefox
- Firefox là ứng dụng phức tạp, nên nếu hữu ích cho việc gỡ lỗi Firefox thì thường cũng có khả năng hữu ích nói chung
- Chú trọng khả năng triển khai thực tế
- rr chạy trên kernel Linux thông thường và phần cứng phổ thông
- Không yêu cầu thay đổi cấu hình hệ thống
- Nhiều kỹ thuật khác đòi hỏi sửa kernel hoặc chạy OS trong máy ảo
- Nhắm tới overhead thời gian chạy thấp
- Để thay thế quy trình làm việc với gdb, nó phải cho kết quả với tốc độ tương đương khi dùng gdb
- Overhead thấp cũng giúp giảm mức độ làm xáo trộn bài kiểm thử
- Coi trọng sự đơn giản trong thiết kế
- Tránh các cách tiếp cận phụ thuộc vào kỹ thuật phức tạp như dynamic binary instrumentation
- Sự đơn giản cũng góp phần vào độ vững chắc và overhead thấp của rr
Hiệu năng và giới hạn
- Overhead của rr thay đổi theo workload ứng dụng
- Trong bộ test của Firefox, hiệu năng ghi ở mức thực dụng
- Trong một số trường hợp, slowdown giảm xuống ≤ 1.2x
- Một bộ test mất 10 phút thì khi ghi bằng rr sẽ mất khoảng 12 phút
- Overhead có thể khác biệt lớn tùy workload
- Với các chương trình chủ yếu đơn luồng, nó có overhead thấp hơn nhiều so với các hệ thống ghi-phát lại cạnh tranh đã biết
- Các giới hạn chính gồm có
- Vì giả lập máy lõi đơn, chương trình song song sẽ bị slowdown do phải chạy như trên một lõi đơn
- Không thể ghi các tiến trình chia sẻ bộ nhớ với tiến trình nằm ngoài cây ghi
- Các tính năng như X shared memory sẽ tự động bị vô hiệu hóa trong tiến trình đang được ghi
- Cần CPU x86 tương đối mới hoặc một số CPU ARM nhất định như Apple M1+
- rr phải biết mọi lời gọi hệ thống mà tiến trình được ghi thực thi
- Dù hỗ trợ phạm vi lời gọi hệ thống rộng cần cho Firefox và nhiều ứng dụng khác, nó vẫn chưa hoàn chỉnh
- Có thể báo cáo lời gọi hệ thống chưa được hỗ trợ qua GitHub issue
- Có thể cần cập nhật để theo kịp thay đổi của kernel, cập nhật thư viện hệ thống hoặc họ CPU mới
Tài liệu tham khảo và cộng đồng
- Extended Technical Report là tài liệu tổng quan về cách rr hoạt động và hiệu năng của nó
- rr wiki đề cập các chủ đề kỹ thuật liên quan đến rr
- Có thể đặt câu hỏi qua mailing list hoặc #rr on chat.mozilla.org
- Cũng có các video demo
1 bình luận
Ý kiến trên Hacker News
Tôi từng dùng kết hợp điểm dừng khi biến thay đổi và reverse-continue trong rr để reverse engineer một codebase lớn, và đạt kết quả rất tốt
Mất một thời gian để bóc tách phần logic cốt lõi nằm sâu bên trong, nhưng rất hữu ích
Cũng đáng nhắc rằng đã có người thử port rr sang Rust, viết được khoảng 60.000 dòng rồi lưu trữ dự án
Có vẻ đây sẽ là một case study thú vị để so sánh tác động, ưu điểm, giới hạn và khó khăn khi viết lại từ C++ sang Rust
https://github.com/sidkshatriya/rd/
Tuy nhiên, lý do lớn khiến việc chuyển nó thành phiên bản chính thức trở nên khó khăn là: 1) rr có rất nhiều bản sửa phức tạp tích lũy trong 10 năm để xử lý các hành vi kernel/process kỳ quặc, nên chúng tôi lo chúng sẽ bị mất trong quá trình port; và 2) dự án nguồn đóng remix[0] được xây trên rr cũng phải được port cùng
[0] https://robert.ocallahan.org/2020/12/rr-remix-efficient-repl...
Tôi không rành Rust, nhưng có vẻ khả năng tương tác với C là tốt. Nếu phần mềm đã hoạt động tốt, tôi thắc mắc lợi ích của việc viết lại bằng Rust là gì
Nhìn sơ thì rr 1.0 mất 3 năm, có khoảng 3–4 đóng góp lớn, và dường như ít nhất 5 người đã đóng góp. rr hiện tại là kết quả có thêm 10 năm làm việc bổ sung trên nền đó
Tôi thắc mắc liệu nó có thật sự chỉ dành cho C/C++ không
Theo hiểu biết hạn chế của tôi, debugger cần danh sách symbol, những thứ như file .pdb trên Windows và phần tương ứng trên Linux, hiểu system call, cùng các thứ tương tự khác. Tôi từng nghĩ nó không quá quan tâm binary đang debug được tạo ra bởi thứ gì, tất nhiên với giả định đó là native code
Không biết rr có hoạt động với các ngôn ngữ như Rust, Zig, Odin, Nim không. Tôi đương nhiên không kỳ vọng với các ngôn ngữ dùng bộ nhớ được quản lý như Python, JS, C#
Khi dùng cùng allocator của Zig thì khá tiện. Khi giải phóng, nó ghi byte 0xaa và không tái sử dụng địa chỉ, nên khả năng crash cao hơn; sau đó có thể đặt watchpoint lên vùng nhớ đó và tua ngược về thời điểm nó được giải phóng
https://github.com/python/devguide/issues/1283
https://morepypy.blogspot.com/2016/07/reverse-debugging-for-...
https://github.com/mesalock-linux/mesapy/blob/mesapy2.7/READ...
Dù nó chỉ cho thấy hệ thống theo góc nhìn của GDB, nó vẫn có thể hoạt động dù là ngôn ngữ thông dịch hay ngôn ngữ biên dịch
Thứ không hoạt động là trường hợp driver trực tiếp cập nhật địa chỉ đã được map. CUDA là một ví dụ như vậy; để replay thì phải mô hình hóa tương tác với driver, và điều đó đã đúng ngay cả trước khi đi đến UVM
Một điểm hay nữa là RR ghi lại cây tiến trình, nên có thể dễ dàng xem các tiến trình khác do executable khởi chạy
Tuy nhiên rr phải bọc tất cả system call, nên nó rất đặc thù Linux và không chạy trên Windows. Định dạng thông tin symbol của Linux là DWARF
rr thật sự rất tuyệt, nhưng hầu như mỗi lần tôi quyết định lôi nó ra như “vũ khí hạng nặng” thì vấn đề lại là bug đồng thời, nên thường rr không tái hiện được
Dù vậy, nếu một số ngôn ngữ đưa rr trực tiếp vào toolchain thì sẽ rất tuyệt. Dĩ nhiên lúc nào cũng có thể “cứ thế” dùng rr/gdb, nhưng hãy tưởng tượng việc cấu hình và dùng rr dễ như pdb của Python
Về cơ bản, nó thường xuyên đổi thread đang chạy để mô phỏng tình huống nhiều core chạy đồng thời. Thực tế nó đã giúp tôi tìm ra vài race condition, dù tất nhiên vẫn có giới hạn
Trong một tác vụ MPI, tôi chỉ gắn rr vào rank 0 và đã tìm ra nơi thứ tự send/recv khác nhau gây ra vấn đề. Hơn nữa đó là một vấn đề khá phức tạp vì liên quan đến mô hình Python gắn với rất nhiều phần sinh native code
Tôi rất muốn dùng, nhưng phần lớn công việc của tôi đều dính đến GPU theo cách nào đó
Lúc thật sự cần thì lại là trên Mac, nhưng tiếc là rr chỉ chạy trên Linux
Có Undodb chạy được trên Mac và có lẽ cũng hỗ trợ multithreading, nhưng đáng tiếc là tốn khoảng 50.000 đô la
Dù dựa trên rr, https://pernos.co/ cũng rất đáng xem, vì nó bổ sung toàn bộ quá trình thực thi chương trình thành một cơ sở dữ liệu có thể truy vấn
Có thể làm được những việc như sau
Cũng có gỡ lỗi ngược tích hợp trong GDB: https://www.sourceware.org/gdb/wiki/ProcessRecord/Tutorial
Tôi cho rằng rr cung cấp nhiều tính năng và độ linh hoạt hơn, nhưng dù sao bản thân GDB cũng đã có thể gỡ lỗi ngược từ khá lâu rồi
Cách triển khai của GDB tiện hơn rr ở chỗ có thể bắt đầu/dừng ghi khi muốn, nhưng hiệu suất thấp hơn vài bậc độ lớn. Nó chỉ đáng dùng cho những đoạn mã rất nhỏ; ngoài ra thì trên thực tế sẽ mất gần như vô hạn thời gian hoặc cạn tài nguyên
Nó chạy được và giúp truy vết lỗi, nhưng chậm đến mức khổ sở. Tôi phải giảm kích thước đầu vào để khiến nó dùng được, và may là sau khi làm vậy vẫn tái hiện được vấn đề
Nếu có thể tìm kiếm nhị phân đến trạng thái chương trình mong muốn với dưới 10000 lần khởi động lại, thì sẽ tốn ít thời gian máy hơn so với dùng thực thi ngược. Trên thực tế, mức chậm lớn đến mức nhiều khi chỉ cần dùng trình gỡ lỗi di chuyển tương tác đến trạng thái gần đúng vài lần rồi khởi động lại chương trình là đủ
Lý do tôi từng dùng được tính năng replay của gdb một cách hữu ích là vì có một tệp đầu vào làm chương trình crash chưa đầy 1 giây sau khi khởi động. Vì vậy, chỉ cần chờ vài phút là có thể lần ngược từ “biến này sai” đến “biến này đã được gán giá trị sai đó như thế nào?”
Trên Windows, có thể dùng WinDbg cho cùng mục đích
Hỗ trợ gỡ lỗi các vấn đề đa luồng tốt hơn
https://www.forrestthewoods.com/blog/windbg-time-travelling-...
rr dùng cách triển khai gỡ lỗi time travel kiểu ghi-phát lại, và nếu được triển khai đúng thì có thể có overhead thấp hơn nhiều. Lần cuối tôi xem, overhead của rr vào khoảng chậm đi 2 lần, và nếu nhớ không nhầm thì tôi từng thấy các trình gỡ lỗi time travel ghi-phát lại khác ở mức khoảng 10%
10% rẻ hơn WinDbg 100 lần, và đủ thấp để luôn bật trong production. Đây là khác biệt thay đổi cuộc chơi
Từ lâu trước đây, VMWare Workstation cũng từng hỗ trợ tính năng như vậy
Không chỉ chương trình ở user space mà cả kernel và driver bên trong VM cũng làm được. Tính năng này được phát hành và tồn tại trong vài phiên bản, rồi bị gỡ bỏ vì chính trị nội bộ
Nó có thể mở rộng bằng plugin, và plugin có toàn quyền kiểm soát CPU đang được mô phỏng. Tôi tự hỏi liệu còn thứ gì tương tự ở đâu đó không
Tôi tự hỏi liệu vấn đề CPU Ryzen của rr giờ chắc chắn đã là chuyện quá khứ chưa
Tôi dùng rr hằng ngày suốt cả ngày để ghi lại việc chạy Firefox trên Threadripper Pro 7950 khá mới, và cũng dùng cùng Pernosco. Wiki rr trên GitHub có giải thích cách làm cho nó hoạt động. Chỉ cần áp dụng một workaround nhỏ là nó chạy rất ổn định
Cũng có các thảo luận cũ
https://news.ycombinator.com/item?id=31617600 (tháng 6/2022)
https://news.ycombinator.com/item?id=18388879 (tháng 11/2018)