Làm thế nào người ta có thể lấy dấu vết ngăn xếp trong C?


83

Tôi biết không có hàm C tiêu chuẩn nào để làm điều này. Tôi đã tự hỏi các kỹ thuật để thực hiện việc này trên Windows và * nix là gì? (Windows XP là hệ điều hành quan trọng nhất của tôi để thực hiện điều này ngay bây giờ.)


Câu trả lời:


81

6
glibc FTW ... một lần nữa. (Đây lại là một lý do tại sao tôi xem xét glibc là tiêu chuẩn vàng tuyệt đối khi nói đến lập trình C (đó và trình biên dịch mà đi với nó).)
Trevor Boyd Smith

4
Nhưng xin chờ chút nữa! Hàm backtrace () chỉ cung cấp một mảng các con trỏ void * đại diện cho các hàm callstack. "Điều đó không hữu ích lắm. Cãi." Đừng sợ! glibc cung cấp một hàm chuyển đổi tất cả các địa chỉ void * (địa chỉ hàm callstack) thành các ký hiệu chuỗi có thể đọc được của con người. char ** backtrace_symbols (void *const *buffer, int size)
Trevor Boyd Smith

1
Cảnh báo: chỉ hoạt động cho các hàm C, tôi nghĩ vậy. @Trevor: Nó chỉ là tìm kiếm syms theo địa chỉ trong bảng ELF.
Conrad Meyer

2
Ngoài ra còn void backtrace_symbols_fd(void *const *buffer, int size, int fd)có thể gửi đầu ra trực tiếp đến stdout / err chẳng hạn.
wkz

2
backtrace_symbols()tệ quá. Nó yêu cầu xuất tất cả các ký hiệu và nó không hỗ trợ các ký hiệu DWARF (gỡ lỗi). libbacktrace là một lựa chọn tốt hơn nhiều trong nhiều (hầu hết) trường hợp.
Erwan Legrand

29

Có backtrace () và backtrace_symbols ():

Từ trang người đàn ông:

#include <execinfo.h>
#include <stdio.h>
...
void* callstack[128];
int i, frames = backtrace(callstack, 128);
char** strs = backtrace_symbols(callstack, frames);
for (i = 0; i < frames; ++i) {
    printf("%s\n", strs[i]);
}
free(strs);
...

Một cách để sử dụng điều này theo cách thuận tiện hơn / OOP là lưu kết quả của backtrace_symbols () trong một hàm tạo lớp ngoại lệ. Vì vậy, bất cứ khi nào bạn ném loại ngoại lệ đó, bạn sẽ có dấu vết ngăn xếp. Sau đó, chỉ cần cung cấp một chức năng để in nó ra. Ví dụ:

class MyException : public std::exception {

    char ** strs;
    MyException( const std::string & message ) {
         int i, frames = backtrace(callstack, 128);
         strs = backtrace_symbols(callstack, frames);
    }

    void printStackTrace() {
        for (i = 0; i < frames; ++i) {
            printf("%s\n", strs[i]);
        }
        free(strs);
    }
};

...

try {
   throw MyException("Oops!");
} catch ( MyException e ) {
    e.printStackTrace();
}

Ta da!

Lưu ý: việc bật cờ tối ưu hóa có thể làm cho dấu vết ngăn xếp kết quả không chính xác. Lý tưởng nhất là người ta sẽ sử dụng khả năng này khi bật cờ gỡ lỗi và tắt cờ tối ưu hóa.


@shuckc chỉ để chuyển đổi địa chỉ thành chuỗi ký hiệu, có thể được thực hiện bên ngoài bằng cách sử dụng các công cụ khác nếu cần.
Woodrow Barlow

22

Đối với Windows, hãy kiểm tra API StackWalk64 () (cũng trên Windows 32bit). Đối với UNIX, bạn nên sử dụng cách gốc của hệ điều hành để làm điều đó hoặc dự phòng cho backtrace của glibc (), nếu có sẵn.

Tuy nhiên, lưu ý rằng sử dụng Stacktrace trong mã gốc hiếm khi là một ý tưởng hay - không phải vì nó không thể thực hiện được, mà bởi vì bạn đang cố gắng đạt được điều sai lầm.

Hầu hết thời gian mọi người cố gắng có được một ngăn xếp, chẳng hạn như một trường hợp ngoại lệ, chẳng hạn như khi bắt được một ngoại lệ, một xác nhận không thành công hoặc - tệ nhất và sai nhất - khi bạn nhận được một "ngoại lệ" nghiêm trọng hoặc tín hiệu như Vi phạm phân đoạn.

Xem xét vấn đề cuối cùng, hầu hết các API sẽ yêu cầu bạn cấp phát bộ nhớ một cách rõ ràng hoặc có thể thực hiện điều đó trong nội bộ. Làm như vậy trong tình trạng mỏng manh mà chương trình của bạn hiện đang ở, về mặt thực tế có thể khiến mọi thứ thậm chí còn tồi tệ hơn. Ví dụ: báo cáo sự cố (hoặc coredump) sẽ không phản ánh nguyên nhân thực sự của sự cố mà do bạn đã không xử lý được).

Tôi cho rằng bạn đang cố gắng đạt được điều đó để xử lý lỗi nghiêm trọng, vì hầu hết mọi người dường như thử điều đó khi nói đến việc có được một stacktrace. Nếu vậy, tôi sẽ dựa vào trình gỡ lỗi (trong quá trình phát triển) và để quá trình coredump trong sản xuất (hoặc kết xuất nhỏ trên windows). Cùng với việc quản lý biểu tượng thích hợp, bạn sẽ không gặp khó khăn khi tìm ra hướng dẫn gây ra sau khi khám nghiệm tử thi.


2
Bạn nói đúng về việc cố gắng phân bổ bộ nhớ trong một trình xử lý tín hiệu hoặc ngoại lệ là rất khó. Một cách giải quyết tiềm năng là phân bổ một lượng không gian "khẩn cấp" cố định khi bắt đầu chương trình hoặc sử dụng bộ đệm tĩnh.
j_random_hacker 19/02/09

Một lối thoát đang tạo ra một dịch vụ coredump, chạy độc lập
Kobor42

5

Bạn nên sử dụng thư viện thư giãn .

unw_cursor_t cursor; unw_context_t uc;
unw_word_t ip, sp;
unw_getcontext(&uc);
unw_init_local(&cursor, &uc);
unsigned long a[100];
int ctr = 0;

while (unw_step(&cursor) > 0) {
  unw_get_reg(&cursor, UNW_REG_IP, &ip);
  unw_get_reg(&cursor, UNW_REG_SP, &sp);
  if (ctr >= 10) break;
  a[ctr++] = ip;
}

Cách tiếp cận của bạn cũng sẽ hoạt động tốt trừ khi bạn thực hiện cuộc gọi từ thư viện được chia sẻ.

Bạn có thể sử dụng addr2linelệnh trên Linux để lấy hàm nguồn / số dòng của PC tương ứng.


"chức năng nguồn / số dòng"? Điều gì sẽ xảy ra nếu liên kết được tối ưu hóa để giảm kích thước mã? Tuy nhiên, tôi sẽ nói rằng đây có vẻ là một dự án hữu ích. Rất tiếc là không có cách nào để có được đăng ký. Tôi chắc chắn sẽ xem xét điều này. Bạn có biết rằng nó hoàn toàn độc lập với bộ xử lý không? Chỉ hoạt động trên bất cứ thứ gì có trình biên dịch C?
Mawg nói Khôi phục Monica

1
Ok, nhận xét này rất đáng giá, nếu chỉ vì đề cập đến lệnh addr2line hữu ích!
Ogre Psalm33, 23/09/10

addr2line không thành công đối với mã có thể định vị lại trên các hệ thống có ASLR (tức là hầu hết những gì mọi người đã sử dụng trong thập kỷ qua).
Erwan Legrand

4

Không có cách nào độc lập với nền tảng để làm điều đó.

Điều gần nhất bạn có thể làm là chạy mã mà không cần tối ưu hóa. Bằng cách đó, bạn có thể đính kèm vào quy trình (sử dụng trình gỡ lỗi c ++ trực quan hoặc GDB) và nhận dấu vết ngăn xếp có thể sử dụng.


Điều đó không giúp ích được gì cho tôi khi sự cố xảy ra trên một máy tính nhúng trong trường. :(
Kevin

@Kevin: Ngay cả trên các máy nhúng, thường có cách để lấy sơ khai trình gỡ lỗi từ xa hoặc ít nhất là kết xuất lõi. Có lẽ không khi nó đã được triển khai tới hiện trường, mặc dù ...
ephemient

nếu bạn chạy bằng gcc-glibc trên nền tảng lựa chọn windows / linux / mac ... thì backtrace () và backtrace_symbols () sẽ hoạt động trên cả ba nền tảng. Với tuyên bố đó, tôi sẽ sử dụng các từ "không có cách nào [di động] để làm điều đó".
Trevor Boyd Smith

4

Đối với Windows, CaptureStackBackTrace()cũng là một tùy chọn, đòi hỏi ít mã chuẩn bị hơn từ phía người dùng StackWalk64(). (Ngoài ra, đối với một kịch bản tương tự mà tôi đã có, CaptureStackBackTrace()kết quả là hoạt động tốt hơn (đáng tin cậy hơn) so với StackWalk64().)


2

Solaris có lệnh pstack , lệnh này cũng đã được sao chép vào Linux.


1
Hữu ích, nhưng không thực sự là C (đó là một tiện ích bên ngoài).
ephemient

1
cũng có, từ mô tả (phần: giới hạn) ": pstack hiện chỉ hoạt động trên Linux, chỉ trên một máy x86 chạy 32 mã nhị phân ELF bit (64 bit không được hỗ trợ)"
Ciro Costa

0

Bạn có thể làm điều đó bằng cách đi ngược lại ngăn xếp. Tuy nhiên, trong thực tế, việc thêm số nhận dạng vào ngăn xếp lệnh gọi ở đầu mỗi hàm và bật nó vào cuối mỗi hàm thường dễ dàng hơn, sau đó chỉ cần thực hiện thao tác in nội dung. Nó hơi giống PITA, nhưng nó hoạt động tốt và cuối cùng sẽ giúp bạn tiết kiệm thời gian.


1
Bạn có thể giải thích rõ hơn về "đi bộ ngược lại"?
Spidey

@Spidey Trên các hệ thống nhúng, đôi khi đây là tất cả những gì bạn có - tôi đoán điều này đã bị bỏ phiếu vì nền tảng là WinXP. Nhưng nếu không có libc hỗ trợ stack walk, về cơ bản bạn phải “đi bộ” stack. Bạn bắt đầu với con trỏ cơ sở hiện tại (trên x86, đây là nội dung của RBP reg). Điều đó đưa bạn đến điểm trên ngăn xếp với 1. RBP đã lưu trước đó (đó là cách tiếp tục hoạt động của ngăn xếp) và 2. địa chỉ trả về cuộc gọi / chi nhánh (lệnh RIP đã lưu của hàm gọi), cho bạn biết hàm là gì. Sau đó, nếu bạn có quyền truy cập vào bảng ký hiệu, bạn có thể tra cứu địa chỉ hàm.
Ted Middleton

0

Trong vài năm qua, tôi đã sử dụng libbacktrace của Ian Lance Taylor. Nó gọn gàng hơn nhiều so với các hàm trong thư viện GNU C yêu cầu xuất tất cả các ký hiệu. Nó cung cấp nhiều tiện ích hơn cho việc tạo backtraces so với libunwind. Và cuối cùng nhưng không kém phần quan trọng, nó không bị đánh bại bởi ASLR cũng như các phương pháp tiếp cận yêu cầu các công cụ bên ngoài như addr2line.

Libbacktrace ban đầu là một phần của bản phân phối GCC, nhưng bây giờ nó đã được tác giả cung cấp dưới dạng thư viện độc lập theo giấy phép BSD:

https://github.com/ianlancetaylor/libbacktrace

Tại thời điểm viết bài, tôi sẽ không sử dụng bất kỳ thứ gì khác trừ khi tôi cần tạo dấu vết trên nền tảng không được hỗ trợ bởi libbacktrace.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.