Bash

如何將可執行 C 程序的錯誤資訊重定向到標準輸出?(MAC OS X)

  • November 2, 2017

我想編寫一個自動 C 程序檢查器。例如,我有一個玩具“hello.c”程序:

#include <stdio.h>

int main()
{
   int a, b;

   while (scanf("%d %d", (&a)-1000000000000000, &b) != EOF)
   {
       printf("%d\n", a+b);
   }

   return 0;
}

這是我的輸入文件“1.in”:

1 2
4 5
10 10
2 2
3 2
7 4

和輸出文件“1.out”:

3
9
20
4
5
11

我使用“gcc hello.c -o hello.o”編譯生成可執行程序“hello.o”。顯然,程序會遇到“段錯誤”:(在我的MAC OS X中執行)

$ ./hello.o <1.in
Segmentation fault: 11

但我想使用管道和差異製作一個自動檢查器:

./hello.o <1.in | diff - 1.out

輸出是:

0a1,6
> 3
> 9
> 20
> 4
> 5
> 11

沒有錯誤資訊顯示!但我想在終端(MAC OS X)中顯示它們。

我嘗試將標準錯誤重定向到標準輸出,例如:

./hello.o <1.in 2>&1 | diff - 1.out

但是沒有效果!

我還嘗試將 stderr 重定向到如下文件:

./hello.o <1.in 2>log

並且資訊“分段錯誤:11”顯示在終端中,而文件中沒有任何內容。

當我使用時會發生同樣的情況

./hello.o <1.in &>log

也許錯誤資訊不在標準錯誤中。

那麼,我該如何解決這個問題呢?謝謝!

**注意:**我已替換hello.ohello,因為.o此上下文中的文件副檔名通常表示目標文件而不是最終的可執行程序。

根據您的文章,您要執行以下命令:

./hello <1.in 2>&1 | diff - 1.out

並且您希望執行時出現的錯誤消息./hello <1.in出現在此命令的輸出中。但是,錯誤消息不是來自hello.o程序本身,而是來自 shell。我能想到的用一行來近似預期效果的最接近的事情是在子shell中執行命令,然後將此輸出與您的diff命令一起使用:

2>&1 bash -c './hello <1.in' | diff - 1.out

這給了我們以下輸出:

1c1,6
< bash: line 1: 58469 Segmentation fault: 11  ./hello < 1.in
---
> 3
> 9
> 20
> 4
> 5
> 11

唯一的區別是,在這種情況下,您會得到一些由 shell 輸出的額外元數據(即行號和命令字元串)。如果你想準確地複制錯誤資訊,那麼你可以使用trap插入一個鉤子來列印出正確的字元串。

我找不到以程式方式提取錯誤消息的方法,所以我轉到Bash 原始碼並蒐索“Segmentation fault”消息。我在一個名為siglist.c的文件中找到了它,以及一堆其他信號和錯誤描述。使用該資訊,我編寫了以下腳本:

#!/bin/bash 

# trapdesc.sh
#
#   Take an error code from the `trap` command and
#   print out the corresponding error message.
#
#   Example usage:
#
#       (trap 'bash trapdesc.sh $?' EXIT; <COMMAND>)
#

# List of signal codes and corresponding error messages
#
# Taken from bash source (siglist.c):
#
#   https://github.com/tpruzina/bash/blob/master/siglist.c
#
declare -a SIGNALS=(
"SIGHUP":"Hangup"
"SIGINT":"Interrupt"
"SIGQUIT":"Quit"
"SIGILL":"Illegal instruction"
"SIGTRAP":"BPT trace/trap"
"SIGABRT":"ABORT instruction"
"SIGEMT":"EMT instruction"
"SIGFPE":"Floating point exception"
"SIGKILL":"Killed"
"SIGBUS":"Bus error"
"SIGSEGV":"Segmentation fault"
"SIGSYS":"Bad system call"
"SIGPIPE":"Broken pipe"
"SIGALRM":"Alarm clock"
"SIGTERM":"Terminated"
"SIGURG":"Urgent IO condition"
"SIGSTOP":"Stopped (signal)"
"SIGTSTP":"Stopped"
"SIGCONT":"Continue"
"SIGCLD":"Child death or stop"
"SIGTTIN":"Stopped (tty input)"
"SIGIO":"I/O ready"
"SIGXCPU":"CPU limit"
"SIGXFSZ":"File limit"
"SIGVTALRM":"Alarm (virtual)"
"SIGPROF":"Alarm (profile)"
"SIGWINCH":"Window changed"
"SIGLOST":"Record lock"
"SIGUSR1":"User signal 1"
"SIGUSR2":"User signal 2"
"SIGMSG":"HFT input data pending"
"SIGPWR":"power failure imminent"
"SIGDANGER":"system crash imminent"
"SIGMIGRATE":"migrate process to another CPU"
"SIGPRE":"programming error"
"SIGGRANT":"HFT monitor mode granted"
"SIGRETRACT":"HFT monitor mode retracted"
"SIGSOUND":"HFT sound sequence has completed"
"SIGINFO":"Information request"
)

# Make sure we get an integer 
if ! [[ "$1" =~ ^[0-9]+$ ]]; then
   2>&1 echo "Not a signal identifier: $1"
   exit 1
fi

# Convert the signal from the `trap` function value to the signal ID
sid="$(($1 - 128))"

# Make sure the signal ID is in the valid range
if [[ "${sid}" -lt 0 || "${sid}" -gt 40 ]]; then
   2>&1 echo "Unrecognized signal: ${sid}"
   exit 1
fi

# Get the array-index for the signal
index="$((sid-1))"

# Get the signal description
description="$(echo ${SIGNALS[index]} | cut -d: -f2)"

# Print the error description
echo "${description}: ${sid}"

現在,使用這個腳本,我們可以執行以下命令:

(trap 'bash trapdesc.sh $?' EXIT; ./hello <1.in)

這會產生與 running 相同的字元串./hello <1.in

Segmentation fault: 11

但是現在您可以從標準錯誤(stderr)中擷取該字元串並將其通過管道傳輸到diff您想要的位置:

(2>&1 trap 'bash trapdesc.sh $?' EXIT; ./hello <1.in) | diff - 1.out

如果錯誤消息已寫入您最初期望的標準輸出,這將產生您將獲得的確切輸出:

1c1,6
< Segmentation fault: 11
---
> 3
> 9
> 20
> 4
> 5
> 11

您已將程序的標準錯誤流重定向到標準輸出,但這不會重定向“分段錯誤”消息,因為該消息不是由您的程序發出的。相反,它是由呼叫程序的 shell 發出的。

你應該在這裡做什麼取決於你的實際目標是什麼。

如果你真的只是想將標準錯誤重定向到標準輸出,你可以用你的 C 程序來做,就像用任何命令一樣。重定向2>&1,以及您使用的其他方法,都可以正常工作。您的程序實際上並沒有向標準錯誤寫入任何內容,但是如果您編寫了一個這樣做的 C 程序*,*那麼您會看到它被重定向。您可以使用fputsorfprintf函式寫入stderr. 例如:

fprintf(stderr, "error: refusing to say hello world\n");

另一方面,如果您的目標是在子程序終止後將您的 shell 寫入的“分段錯誤”消息重定向到標準錯誤SIGSEGV,那麼您可以編寫一個包含對您的程序的呼叫的複合命令,並從該複合命令重定向命令。您不必有任何其他命令;正如Gilles 解釋的那樣,將您的一個命令包含在其中就足夠了{ ;}。例如,您可以執行:

{ ./hello.o 1000; } 2>>outfile

這會將執行程序產生的所有標準錯誤輸出(包括程序產生的輸出(在本例中為無)和 shell 產生的輸出)附加到文件outfile.

(但是,我懷疑您確實只想重定向程序可能實際產生的錯誤輸出,這就是為什麼我要回答這個問題而不是將其標記為重複關閉。)


儘管讓您的 C 程序寫入一個明顯偽造的地址似乎是故意觸發分段錯誤的可靠方法,但實際上並非如此。這是因為允許 C 編譯器假設未發生未定義的行為,並且某些編譯器即使在不使用高級優化進行編譯時也會利用此假設。要測試程序在分段錯誤下的行為,我建議您SIGSEGV在程序執行時向它發送信號。它可以使用raise函式對自己執行此操作,也可以使用killorkillall命令執行此操作。

例如,為了測試上面的範例,我只是{ sleep 1000; } >&out在另一個終端中執行了killall -SEGV sleep. (由於該sleep命令可能在後台使用,我應該提醒您,您可能不想在生產機器上遵循這個精確的過程,至少不是作為正在做其他重要事情的使用者,當然不是作為 root。)

最後,您可能不想用.o後綴命名您的執行檔,因為這些後綴通常用於編譯器生成的目標文件,這些目標文件必須連結在一起才能生成可以實際執行的文件。在類 Unix 作業系統上,可執行二進製文件通常不帶副檔名。

引用自:https://unix.stackexchange.com/questions/401983