Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 35.624 phút đọc

Quy trình sáu bước và tệp .gdbinit

Sau bài này bạn sẽ làm được

  • Áp dụng quy trình sáu bước cho một lỗi thật
  • Thu hẹp phạm vi bằng cách chia đôi
  • Viết tệp .gdbinit với các lệnh tự định nghĩa
  • Kết thúc mọi lần sửa lỗi bằng một bài kiểm thử hồi quy

Gỡ lỗi giỏi không phải là biết nhiều lệnh GDB. Đó là có một quy trình, và bám vào quy trình đó cả khi đang bực. Hai bước hay bị bỏ nhất là bước đầu và bước cuối, và đó cũng là hai bước quan trọng nhất.

#Sáu bước

Bỏ qua bước 1 nghĩa là bạn sửa mò. Bỏ qua bước 6 nghĩa là lỗi sẽ quay lại.
  1. Tái hiện lỗi một cách ổn định

    Tìm đầu vào nhỏ nhất gây lỗi. Nếu lỗi chỉ xuất hiện một lần trong mười lần chạy thì bạn không biết mình đã sửa hay chưa.

  2. Thu hẹp phạm vi

    Chia đôi: xóa dần mã, dữ liệu vào, hoặc lịch sử phiên bản cho tới khi lỗi biến mất. Mỗi lần chia đôi giảm một nửa vùng phải tìm.

  3. Chạy sanitizer

    ASan và UBSan trước, Valgrind sau. Chúng không cần bạn có giả thuyết và thường chỉ thẳng ra dòng lỗi.

  4. Mở GDB

    Đặt điểm dừng trước chỗ nghi ngờ, không phải tại chỗ sập. Đọc bt và info locals.

  5. Dùng watchpoint nếu một biến đổi bất thường

    Đây là bước riêng vì nó giải quyết một loại câu hỏi mà bốn bước trên không giải quyết được.

  6. Viết kiểm thử hồi quy rồi mới sửa

    Bài kiểm thử phải thất bại trên bản chưa sửa. Nếu nó đạt thì nó không kiểm thử đúng thứ bạn nghĩ.

#Bước một: tái hiện

# Co dinh cac yeu to ngau nhien de loi tai hien duoc

# 1. So ngau nhien: dat hat co dinh
srand(12345);              /* thay cho srand(time(NULL)) */

# 2. Dia chi cap phat: tat ngau nhien hoa khong gian dia chi
setarch -R ./prog          # Linux
(gdb) set disable-randomization on    # GDB tat san, mac dinh

# 3. Bo nho chua khoi tao: bat cho no mot mau co dinh
MALLOC_PERTURB_=42 ./prog  # glibc: dien 42 vao bo nho moi cap
                           # va 213 vao bo nho vua free

# 4. Da luong: giam so luong
./prog --so-luong=1        # neu chuong trinh cho phep

# 5. Moi truong: chay trong moi truong toi thieu
env -i ./prog

#Bước hai: thu hẹp

Chia đôi dữ liệu vào

# Tep dau vao 10000 dong lam chuong trinh sap.

head -5000 vao.txt > a.txt && ./prog a.txt     # sap? -> loi o nua dau
tail -5000 vao.txt > b.txt && ./prog b.txt     # sap? -> loi o nua sau

# Lap lai. Sau 14 lan chia doi tu 10000 dong, ban con 1 dong.

# Cong cu tu dong hoa viec nay:
#   ddmin, delta debugging
#   creduce, cvise: rut gon MA NGUON C giu nguyen mot trieu chung

Chia đôi lịch sử phiên bản

# "Thang truoc con chay dung, gio thi sap." Git tim ho ban:

git bisect start
git bisect bad                    # ban hien tai co loi
git bisect good v1.2.0            # ban nay chay dung

# Git checkout mot ban o giua. Ban chay thu roi bao:
git bisect good                   # hoac
git bisect bad

# Lap lai. Voi 1000 ban ghi, chi can 10 lan thu.

git bisect reset                  # xong, quay ve nhanh cu

# Tu dong hoan toan, neu ban co mot lenh kiem tra:
git bisect run ./kiem_tra.sh      # tra ve 0 la tot, khac 0 la loi

Chia đôi mã nguồn

/* Xoa dan cho toi khi loi bien mat. Moi buoc phai kiem tra lai. */

/* Vong 1: bo mot nua so ham goi trong main */
/* Vong 2: bo mot nua con lai */
/* ...

   Cach nay tho nhung hieu qua. Khi ban con hai muoi dong ma van
   sap, viec doc ma tro nen de dang.

   Meo: dung "#if 0 ... #endif" thay vi xoa that, de con khoi phuc.
   Bai 19.6 da noi. */

/* Va mot bien the tinh te hon: chia doi CO DICH */

gcc -O2 ...        # sap
gcc -O1 ...        # chay dung  -> loi lien quan toi toi uu o -O2
gcc -O2 -fno-strict-aliasing ...   # chay dung -> tim ra thu pham

#Bước sáu: kiểm thử hồi quy

Viết kiểm thử TRƯỚC khi sửa
/* Dung khung kiem thu ba macro cua Bai 32.4 */

#include "test.h"
#include "sinhvien.h"

static void thu_loi_ghi_de(void) {
    NHOM("loi #142: ten qua dai ghi de len diem");

    SinhVien sv = { "", 100 };

    dat_ten(&sv, "NguyenVanA");        /* 10 ky tu vao mang 8 byte */

    KIEM_BANG(sv.diem, 100);           /* diem KHONG duoc doi */
    KIEM_BANG(sv.ten[7], 0);           /* co ky tu ket thuc chuoi */
    KIEM_CHUOI(sv.ten, "NguyenV");     /* bi cat dung bay ky tu */
}
terminal
# Chạy TRƯỚC khi sửa: phải thất bại
make kiem
[loi #142: ten qua dai ghi de len diem]
  HONG thu.c:12: sv.diem == 100 (16750 vs 100)
  HONG thu.c:13: sv.ten[7] == 0 (110 vs 0)
  HONG thu.c:14: "NguyenVanA" vs "NguyenV"

3 kiem tra, 3 hong
# Chạy SAU khi sửa: phải đạt
make kiem
[loi #142: ten qua dai ghi de len diem]

3 kiem tra, 0 hong

#Tệp .gdbinit

~/.gdbinit
# --- hien thi ---
set print pretty on              # struct xuong dong, thut le
set print array on               # mang moi phan tu mot dong
set print array-indexes on       # kem chi so: [0] = 5
set print elements 200           # so phan tu toi da in ra
set print repeats 10             # gom gia tri lap lai
set print null-stop on           # chuoi dung o ky tu ket thuc
set print union on               # in ca thanh vien union

# --- tien lam viec ---
set pagination off               # khong hoi "Press enter to continue"
set confirm off                  # khong hoi lai khi quit hay delete
set history save on              # nho lenh giua cac phien
set history size 10000
set history filename ~/.gdb_history

# --- hop ngu ---
set disassembly-flavor intel     # cu phap Intel, de doc hon AT&T

# --- go loi ---
set disable-randomization on     # mac dinh da bat, ghi ro cho chac
set backtrace past-main off      # khong hien ruot cua libc truoc main

# --- cho phep nap .gdbinit trong thu muc du an ---
add-auto-load-safe-path ~/duan

#Lệnh tự định nghĩa

./.gdbinit của một dự án
# --- khoi dong nhanh ---
define bm
  break main
  run
end
document bm
Dat diem dung o main va chay ngay.
end

# --- in danh sach lien ket ---
define in_ds
  set $p = $arg0
  set $i = 0
  while $p != 0
    printf "[%d] ma=%-4d ten=%s\n", $i, $p->ma, $p->ten
    set $p = $p->sau
    set $i = $i + 1
  end
  printf "tong cong %d nut\n", $i
end
document in_ds
In toan bo danh sach lien ket. Vi du: in_ds dau
end

# --- in mot sinh vien cho de doc ---
define in_sv
  printf "SinhVien{ ma=%d ten=%s diem=%d }\n", \
         $arg0->ma, $arg0->ten, $arg0->diem
end

# --- do bo nho mot khoi ---
define do_khoi
  printf "=== %d byte tu %p ===\n", $arg1, $arg0
  x/$arg1xb $arg0
end
document do_khoi
Do n byte tu mot dia chi. Vi du: do_khoi p 64
end

# --- ngu canh day du khi dung ---
define nc
  printf "=== vet goi ===\n"
  bt
  printf "=== bien cuc bo ===\n"
  info locals
  printf "=== tham so ===\n"
  info args
end
document nc
In vet goi, bien cuc bo va tham so cua khung hien tai.
end
Khi ngôn ngữ lệnh của GDB không đủ: Python
# Trong .gdbinit hoac mot tep .py rieng

python
import gdb

class InDanhSach(gdb.Command):
    """In danh sach lien ket. Cu phap: in_ds <nut_dau>"""

    def __init__(self):
        super().__init__("in_ds", gdb.COMMAND_USER)

    def invoke(self, doi_so, tu_man_hinh):
        p = gdb.parse_and_eval(doi_so)
        i = 0

        while p != 0:
            print(f"[{i}] ma={int(p['ma']):<4} ten={p['ten'].string()}")
            p = p["sau"]
            i += 1

            if i > 10000:
                print("qua 10000 nut, co the co vong")
                break

        print(f"tong cong {i} nut")

InDanhSach()
end

# Python trong GDB con lam duoc:
#   - pretty printer: doi cach GDB hien mot kieu do ban dinh nghia
#   - doc va ghi file, goi ra ngoai
#   - phan tich cau truc du lieu phuc tap
#
# Moi ban GDB hien dai deu co san Python. Kiem tra:
#   (gdb) python print(gdb.VERSION)

Tự làm thử

  1. Chạy một chương trình với MALLOC_PERTURB_=42 và xem có lỗi mới lộ ra không.
  2. Dùng git bisect để tìm bản ghi làm hỏng một bài kiểm thử.
  3. Viết một bài kiểm thử hồi quy và xác nhận nó thất bại trên bản chưa sửa.
  4. Viết tệp ~/.gdbinit theo mẫu trong bài.
  5. Viết lệnh in_ds và nc rồi dùng chúng trên một chương trình thật.
  6. Viết một pretty printer bằng Python cho một struct của bạn.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • Sáu bước: tái hiện, thu hẹp, sanitizer, GDB, watchpoint, kiểm thử hồi quy.
  • MALLOC_PERTURB_=42 biến nhiều lỗi thỉnh thoảng thành lỗi luôn xảy ra, chỉ bằng một biến môi trường.
  • git bisect run tự động tìm bản ghi gây lỗi trong mười lần thử với một nghìn bản ghi.
  • Bài kiểm thử hồi quy phải thất bại trên bản chưa sửa, nếu không nó không chứng minh được gì.
  • Lệnh tự định nghĩa trong .gdbinit của dự án nên đưa vào kho mã để cả nhóm dùng chung.