1 điểm bởi GN⁺ 4 giờ trước | 1 bình luận | Chia sẻ qua WhatsApp
  • Khi biên dịch và chạy lặp lại chương trình C bằng entr, hello kết thúc bất thường, nhưng sau phần output biên dịch không xuất hiện thông báo Segmentation fault
  • Thông báo này không do chương trình bị crash in ra, mà do shell cha in ra sau khi thu hồi tiến trình con và xác nhận nó đã kết thúc vì SIGSEGV
  • Ngay cả khi chỉ định bash -c, nếu về thực chất chỉ còn một lệnh cần chạy, Bash có thể tự thay thế chính nó bằng execve thay vì tạo tiến trình mới, khiến shell cha chịu trách nhiệm in thông báo biến mất
  • Trong một script riêng, Bash được khởi động bằng shebang sẽ chạy ./hello như tiến trình con và chờ nó, nên khi xác nhận SIGSEGV sẽ in ra Segmentation fault (core dumped)
  • Có thể thấy thông báo lỗi bằng cách chạy ./hello trong subshell (./hello) hoặc thêm ; true phía sau để còn việc tiếp theo, qua đó ngăn tối ưu hóa exec

Luồng thực thi khiến thông báo biến mất

  • Dùng lệnh entr sau để lặp lại việc biên dịch và chạy mỗi khi hello.c thay đổi

    ls hello.c | entr -s "gcc -o hello hello.c && ./hello"
    
  • hello kết thúc vì lỗi segmentation fault, không để lại output nào, không có thông báo Segmentation fault hay trạng thái thoát khác 0 nào nhìn thấy được

  • Kể cả bọc lệnh bằng bash -c, kết quả vẫn như vậy

    ls hello.c | entr -s "bash -c 'gcc -o hello hello.c && ./hello'"
    
  • Ngược lại, nếu chuyển cùng lệnh vào run.sh rồi chạy thì lỗi được hiển thị

    #!/bin/bash
    gcc -o hello hello.c && ./hello
    
    ls hello.c | entr -s ./run.sh
    ./run.sh: line 2: 104465 Segmentation fault (core dumped) ./hello
    
  • Chủ thể in ra Segmentation fault

    • Chương trình bị crash lúc đó đã kết thúc; shell cha thu hồi tiến trình con, xác nhận nó chết vì SIGSEGV, rồi in thông báo
    • Vì vậy nếu không còn shell cha để in thông báo, sẽ không có gì xuất hiện
    • Trong bash -c "some_command", nếu việc cần chạy về thực chất chỉ là lệnh đó, Bash có thể không fork tiến trình mới mà tự thay thế chính nó bằng execve
    • Tối ưu hóa này không lộ ra bên ngoài trong phần lớn các lần thực thi
    • Với cách dùng script, entr chạy ./run.sh như tiến trình con, và shebang khởi động một Bash mới
    • Bash mới sau khi chạy gcc sẽ fork và chờ ./hello, nên có thể xác nhận tiến trình con kết thúc vì SIGSEGV và để lại thông báo lỗi

Hai cách giữ lại shell cha

  • Nếu chạy lệnh gây crash trong subshell, thông báo sẽ xuất hiện trở lại
    ls hello.c | entr -s "gcc -o hello hello.c && (./hello)"
    hello.c: line 1: 106595 Segmentation fault (core dumped) ( ./hello )
    
    • Dấu ngoặc buộc ./hello được chạy trong một subshell đã được fork
    • Vì Bash vẫn giữ vai trò cha, nó có thể thu hồi kết thúc SIGSEGV của subshell và in thông báo
  • Nếu để lại một việc cần chạy sau lệnh gây crash, cũng có thể ngăn tối ưu hóa exec
    ls hello.c | entr -s "bash -c 'gcc -o hello hello.c && ./hello; true'"
    bash: line 1: 109516 Segmentation fault (core dumped) ./hello
    
    • Do có ; true, Bash vẫn còn việc phải làm sau khi ./hello kết thúc, nên không tự thay thế chính nó bằng chương trình đó
    • Có thể thấy thông báo lỗi segmentation fault ngay cả khi không có script wrapper riêng

1 bình luận

 
Ý kiến trên Lobste.rs
  • Các bản phân phối Linux mới nhất mặc định vô hiệu hóa core dump, nên nhiều lúc khá phiền
    Có thể bật lại bằng ulimit -c unlimited, nhưng có vẻ file core cũng không được tạo trong cùng thư mục với tệp thực thi, và hình như systemd chuyển nó đi đâu đó
    Sẽ hay hơn nếu giống OpenBSD, bật core dump theo mặc định và đặt nó cùng thư mục với tệp thực thi, nhưng hiện giờ khi xảy ra crash thì phần lớn phải chạy lại tệp thực thi trong gdb

    • Trên systemd, có thể dùng coredumpctl để liệt kê và lấy các core dump gần đây
    • Việc đặt core dump ở nơi có tệp thực thi có vẻ là một lựa chọn khá kỳ lạ
      Người ta đâu có nghĩ đến chuyện tìm core dump trong những nơi như /usr/bin, hơn nữa các vị trí đó thường nằm trên hệ thống tệp dung lượng nhỏ hoặc chỉ đọc
      Có lẽ thư mục làm việc sẽ phù hợp hơn nhiều làm vị trí mặc định
    • Lý do core file bị vô hiệu hóa theo mặc định có lẽ là vì các chuyên gia bảo mật cho rằng người dùng không thể vận hành Linux an toàn, và các core file bị bỏ quên có thể bị lợi dụng
      Thậm chí ngay cả khi người dùng bật core file, dường như hệ thống vẫn cho rằng họ không biết mình đang làm gì, nên nó bị giấu ở nơi mà ngay cả root cũng khó tìm
  • Nếu viết chương trình bằng C, D hoặc ngôn ngữ tương tự và muốn biết nguyên nhân kết thúc, thì nên kiểm tra giá trị trả về của lời gọi hệ thống wait, từ đó có thể biết chương trình có bị kết thúc bởi tín hiệu hay không
    Nếu hệ điều hành chuyển core dump sang nơi khác thì cũng có lợi thế là có thể gỡ lỗi ngay khi cần hoặc gửi báo cáo lỗi cho bản phân phối
    Khi tạo core dump, nhân Linux sẽ đọc tệp đặc biệt /proc/sys/kernel/core_pattern để quyết định nơi lưu
    Thay vì ghi ra file, nó cũng có thể chuyển qua pipe cho một chương trình, nhờ đó có thể hiện hộp thoại GUI, chạy debugger ngay lập tức hoặc gửi email báo cáo cho nhà phát triển
    systemd đặt core_pattern của kernel thành giá trị do nó quản lý, nhưng ngay cả khi không dùng systemd thì vẫn có thể dùng cùng tính năng này của kernel
    Cũng có câu chuyện rằng trong thời phát triển Windows XP, việc gửi core dump về Microsoft đã giúp sửa nhiều lỗi mà nếu không thì họ còn chẳng biết tồn tại; và nếu cài công cụ phát triển trên Windows thì debugger theo yêu cầu cũng khá hữu ích khi phát triển chương trình của riêng mình
    Tính năng này được đưa vào Linux từ năm 2003, và có thể xem chi tiết trong man 5 core

  • Core dump rất tuyệt cho việc gỡ lỗi, nhưng nếu nằm cạnh những file có vẻ không liên quan thì sẽ khiến người dùng phổ thông bối rối, và nếu dùng như một hệ thống báo cáo chẩn đoán thì có thể rất tệ
    Tôi đã sốc khi lần đầu thấy lượng dữ liệu được đưa vào các báo cáo crash gửi từ Windows
    Nếu giả định có một hệ thống báo cáo crash bảo vệ quyền riêng tư, thì cần học cách suy ra nguyên nhân chỉ từ thông tin stack và thanh ghi để có thể chẩn đoán các crash đã được gửi lên

  • Tôi hoàn toàn không biết rằng Bash đôi khi trực tiếp exec các lệnh trong script, và đây là một tính năng khá bất ngờ
    Bản thân tiến trình Bash cũng không tạo ra tải lớn đến thế, và ở những chỗ cần thiết tôi vốn đã có thói quen dùng exec, nên tính năng này có cảm giác hơi được thiết kế sai
    Tuy vậy, nếu cộng tổng số lần thực thi lại thì có lẽ nó vẫn có thể tạo ra khác biệt lớn hơn dự đoán