使用 C-Reduce 進行調試

做者:Mike Ash,原文連接,原文日期:2018-06-29 譯者:BigNerdCoding;校對:pmstnumbbbbb;定稿:Forelaxhtml

調試複雜問題自己就並不輕鬆,若是尚未足夠的上下文和大體方向的話那就是一件很是困難的事。因此對代碼進行精簡縮小調試範圍也就變成了一種常見的行爲。不過與繁雜的手動簡化相比,執行自動化處理程序明顯更容易發揮計算機自身的優點。C-Reduce 正是爲此而生,它能自動對原始代碼進行簡化操做輸出一個簡化的調試友好版本。下面咱們看看如何使用該自動化程序。git

概述

C-Reduce 代碼基於兩個主要思想。github

首先,C-Reduce 經過刪除相關代碼行或者將 token 重命名爲更短的版本等手段,將某些原始代碼轉化爲一個簡化版本。shell

其次,對簡化結果進行檢驗測試。上面的代碼簡化操做是盲目的,所以常常產生不含待跟蹤錯誤甚至是根本沒法編譯的簡化版本。因此在使用 C-Reduce 時,除原始代碼外還須要一個用來測試簡化操做是否符合特定「預期」的腳本程序。而「預期」的標準則由咱們根據實際狀況進行設定。例如,若是你想定位到某個 bug 那麼「預期」就意味着簡化版本包含與原始代碼一致的錯誤。你能夠利用腳本程序寫出任何你想要的「預期」標準,C-Reduce 會依據該腳本程序確保簡化版本符合預先定義的行爲。編程

安裝

C-Reduce 程序的依賴項很是多,安裝也很複雜。好在有 Homebrew 的加持,咱們只需輸入如下命令便可:swift

brew install creduce
複製代碼

若是你想手動安裝的話,能夠參照該安裝 指南xcode

簡易示例

想出一個小的示例代碼解釋 C-Reduce 是很困難的,由於它的主要目的是從一個大的程序簡化出一個小型示例。下面是我不遺餘力想出來的一個簡單 C 程序代碼,它會產生一些難以理解的編譯警告。bash

$ cat test.c
#include <stdio.h>

struct Stuff {
    char *name;
    int age;
}

main(int argc, char **argv) {
    printf("Hello, world!\n");
}

$ clang test.c
test.c:3:1: warning: return type of 'main' is not 'int' [-Wmain-return-type]
struct Stuff {
^
test.c:3:1: note: change return type to 'int'
struct Stuff {
^~~~~~~~~~~~
int
test.c:10:1: warning: control reaches end of non-void function [-Wreturn-type]
}
^
2 warnings generated.
複製代碼

從警告中咱們知道 structmain 代碼存在某種問題!至於具體問題是什麼,咱們能夠在簡化版本中仔細分析。併發

C-Reduce 能輕鬆的將程序精簡到遠超咱們想象的程度。因此爲了控制 C-Reduce 的精簡行爲確保簡化操做符合特定預期,咱們將編寫一個小的 shell 腳本,編譯該段代碼並檢查警告信息。在該腳本中咱們須要匹配編譯警告並拒絕任何形式編譯錯誤,同時咱們還須要確保輸出文件包含 struct Stuff,詳細腳本代碼以下:app

#!/bin/bash 
clang test.c &> output.txt
grep error output.txt && exit 1
grep "warning: return type of 'main' is not 'int'" output.txt &&
grep "struct Stuff" output.txt
複製代碼

首先,咱們對簡化代碼進行編譯並將輸出重定向到 output.txt。若是輸出文件包含任何 "error" 字眼則當即退出並返回狀態碼 1。不然腳本將會繼續檢查輸出文本是否包含特定警告信息和文本 struct Stuff。當 grep 同時成功匹配上述兩個條件時,會返回狀態碼 0;不然就退出並返回狀態碼 1。狀態碼 0 表示符合預期而狀態碼 1 則表示簡化的代碼不符合預期須要從新簡化。

接下來咱們運行 C-Reduce 看看效果:

$ creduce interestingness.sh test.c 
===< 4907 >===
running 3 interestingness tests in parallel
===< pass_includes :: 0 >===
(14.6 %, 111 bytes)

...lots of output...

===< pass_clex :: rename-toks >===
===< pass_clex :: delete-string >===
===< pass_indent :: final >===
(78.5 %, 28 bytes)
===================== done ====================

pass statistics:
  method pass_balanced :: parens-inside worked 1 times and failed 0 times
  method pass_includes :: 0 worked 1 times and failed 0 times
  method pass_blank :: 0 worked 1 times and failed 0 times
  method pass_indent :: final worked 1 times and failed 0 times
  method pass_indent :: regular worked 2 times and failed 0 times
  method pass_lines :: 3 worked 3 times and failed 30 times
  method pass_lines :: 8 worked 3 times and failed 30 times
  method pass_lines :: 10 worked 3 times and failed 30 times
  method pass_lines :: 6 worked 3 times and failed 30 times
  method pass_lines :: 2 worked 3 times and failed 30 times
  method pass_lines :: 4 worked 3 times and failed 30 times
  method pass_lines :: 0 worked 4 times and failed 20 times
  method pass_balanced :: curly-inside worked 4 times and failed 0 times
  method pass_lines :: 1 worked 6 times and failed 33 times

		 ******** .../test.c ********

struct Stuff {
} main() {
}
複製代碼

最終咱們獲得一個符合預期的簡化版本,而且會覆蓋原始代碼文件。因此在使用 C-Reduce 時須要注意這一點!必定要在代碼的副本中運行 C-Reduce 進行簡化操做,不然可能對原始代碼形成不可逆更改。

該簡化版本使代碼問題成功暴露了出來:在 struct Stuff 類型聲明末尾忘記加分號,另外 main 函數沒有明確返回類型。這致使編譯器將 struct Stuff 錯誤的看成了返回類型。而 main 函數必須返回 int 類型,因此編譯器發出了警告。

Xcode 工程

對於單個文件的簡化來講 C-Reduce 很是棒,可是更復雜場景下效果如何呢?咱們大多數人都有多個 Xcode 工程,那麼如何簡化某個 Xcode 工程呢?

考慮到 C-Reduce 的工做方式,簡化 Xcode 工程並不簡單。它會將須要簡化的文件拷貝到一個目錄中,而後運行腳本。這樣雖然可以同時運行多個簡化任務,但若是須要其餘依賴才能讓它工做,那麼就可能沒法簡化。好在能夠在腳本中運行各類命令,因此能夠將項目的其他部分複製到臨時目錄來解決這個問題。

我使用 Xcode 建立了一個標準的 Objective-C 語言的 Cocoa 應用,而後對 AppDelegate.m 進行以下修改:

#import "AppDelegate.h"

@interface AppDelegate () {
    NSWindow *win;
}

@property (weak) IBOutlet NSWindow *window;
@end

@implementation AppDelegate

- (void)applicationDidFinishLaunching: (NSRect)visibleRect {
    NSLog(@"Starting up");
    visibleRect = NSInsetRect(visibleRect, 10, 10);
    visibleRect.size.height *= 2.0/3.0;
    win = [[NSWindow alloc] initWithContentRect: NSMakeRect(0, 0, 100, 100) styleMask:NSWindowStyleMaskTitled backing:NSBackingStoreBuffered defer:NO];
	
    [win makeKeyAndOrderFront: nil];
    NSLog(@"Off we go");
}

@end
複製代碼

這段代碼會讓應用在啓動時崩潰:

* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
	  * frame #0: 0x00007fff3ab3bf2d CoreFoundation`__CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 13
複製代碼

上面的內容並非一個很是有用的調用棧信息。雖然咱們能夠經過調試追溯問題,可是這裏咱們嘗試使用 C-Reduce 來進行問題定位。

這裏的 C-Reduce 預期定義將包含更多的內容。首先咱們須要給應用設置運行的超時時間。咱們會在運行時進行崩潰捕獲操做,若是沒有發生崩潰則保持應用正常運行直到觸發超時處理而退出。下面是一段網上隨處可見的 perl 腳本代碼:

function timeout() { perl -e 'alarm shift; exec @ARGV' "$@"; }
複製代碼

緊接着咱們須要拷貝該工程文件:

cp -a ~/Development/creduce-examples/Crasher .
複製代碼

而後將修改後的 AppDelegate.m 文件拷貝到合適的路徑下。(注意:若是文件發現合適簡化版本,C-Reduce 會將文件複製回來,因此必定要在這裏使用 cp 而不是 mv。使用 mv 會致使一個奇怪的致命錯誤。)

cp AppDelegate.m Crasher/Crasher
複製代碼

接下來咱們切換到 Crasher 目錄執行編譯命令,並在發生錯誤時退出。

cd Crasher
xcodebuild || exit 1
複製代碼

若是編譯成功,則運行應用而且設置超時時間。個人系統對編譯項進行了設置,因此 xcodebuild 命令會將編譯結果存放着本地 build 目錄下。由於配置可能存在差別,因此你首先須要自行檢查。若是你將配置設爲共享構建目錄的話,那麼須要在命令行中增長 —n 1 來禁用 C-Reduce 的併發構建操做。

timeout 5 ./build/Release/Crasher.app/Contents/MacOS/Crasher
複製代碼

若是應用發生崩潰的話,那麼會返回特定狀態碼 139 。此時咱們須要將其轉化爲狀態碼 0 ,其它情形通通返回狀態碼 1。

if [ $? -eq 139 ]; then
    exit 0
else
    exit 1
fi
複製代碼

緊接着,咱們運行 C-Reduce:

$ creduce interestingness.sh Crasher/AppDelegate.m
...
(78.1 %, 151 bytes)
===================== done ====================

pass statistics:
  method pass_ints :: a worked 1 times and failed 2 times
  method pass_balanced :: curly worked 1 times and failed 3 times
  method pass_clex :: rm-toks-7 worked 1 times and failed 74 times
  method pass_clex :: rename-toks worked 1 times and failed 24 times
  method pass_clex :: delete-string worked 1 times and failed 3 times
  method pass_blank :: 0 worked 1 times and failed 1 times
  method pass_comments :: 0 worked 1 times and failed 0 times
  method pass_indent :: final worked 1 times and failed 0 times
  method pass_indent :: regular worked 2 times and failed 0 times
  method pass_lines :: 8 worked 3 times and failed 43 times
  method pass_lines :: 2 worked 3 times and failed 43 times
  method pass_lines :: 6 worked 3 times and failed 43 times
  method pass_lines :: 10 worked 3 times and failed 43 times
  method pass_lines :: 4 worked 3 times and failed 43 times
  method pass_lines :: 3 worked 3 times and failed 43 times
  method pass_lines :: 0 worked 4 times and failed 23 times
  method pass_lines :: 1 worked 6 times and failed 45 times

******** /Users/mikeash/Development/creduce-examples/Crasher/Crasher/AppDelegate.m ********

#import "AppDelegate.h"
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSRect)a {
    a = NSInsetRect(a, 0, 10);
    NSLog(@"");
}
@end
複製代碼

咱們獲得一個極其精簡的代碼。雖然 C-Reduce 沒有移除 NSLog 那行代碼,可是崩潰看起來並非它引發的。因此此處致使崩潰的代碼只能是 a = NSInsetRect(a, 0, 10); 這行代碼。經過檢查該行代碼的功能和使用到的變量,咱們能發現它使用了一個 NSRect 類型的變量而 applicationDidFinishLaunching 函數的入參實際上並非該類型。

- (void)applicationDidFinishLaunching:(NSNotification *)notification;
複製代碼

所以該崩潰應該是因爲類型不匹配致使的錯誤引發的。

由於編譯工程的耗時遠超過單文件並且不少測試示例都會觸發超時處理,因此此例中的 C-Reduce 運行時間會比較長。C-Reduce 會在每次運行成功後將精簡的文件寫回原始文件,因此你可使用文本編輯器保持文件的打開狀態並查看更改結果。另外你能夠在合適時時機運行 ^C 命令結束 C-Reduce 執行,此時會獲得部分精簡過的文件。若是有必要你後續能夠在此基礎上繼續進行精簡工做。

Swift

若是您使用 Swift 而且也有精簡需求時該怎麼辦呢?從名字上來看,我本來覺得 C-Reduce 只適用於 C(也許還包括 C++,由於不少工具都是如此)。

不過好在,此次個人直覺錯了。C-Reduce 確實有一些與 C 相關的特定驗證測試,但大部分仍是和語言無關的。不管你使用何種語言只要你能寫出相關的驗證測試,C-Reduce 都能派上用場,雖然效率可能不是很理想。

下面咱們就來試一試。我在 bugs.swift.org 上面找到了一個很好的測試 用例。不過該崩潰只出如今 Xcode9.3 版本上,而我正好就安裝了該版本。下面是該 bug 示例的簡易修改版:

import Foundation

func crash() {
    let blah = ProblematicEnum.problematicCase.problematicMethod()
    NSLog("\(blah)")
}

enum ProblematicEnum {
    case first, second, problematicCase

    func problematicMethod() -> SomeClass {
    	let someVariable: SomeClass

    	switch self {
    	case .first:
    	    someVariable = SomeClass()
    	case .second:
    	    someVariable = SomeClass()
    	case .problematicCase:
    	    someVariable = SomeClass(someParameter: NSObject())
    	    _ = NSObject().description
    	    return someVariable // EXC_BAD_ACCESS (simulator: EXC_I386_GPFLT, device: code=1)
    	}

    	let _ = [someVariable]
    	return SomeClass(someParameter: NSObject())
    }

}

class SomeClass: NSObject {
    override init() {}
    init(someParameter: NSObject) {}
}

crash()
複製代碼

當咱們嘗試在啓用優化的狀況下運行代碼時,會出現以下結果:

$ swift -O test.swift 
<unknown>:0: error: fatal error encountered during compilation; please file a bug report with your project and the crash log
<unknown>:0: note: Program used external function '__T04test15ProblematicEnumON' which could not be resolved!
...
複製代碼

與之對應的驗證腳本爲:

swift -O test.swift
if [ $? -eq 134 ]; then
    exit 0
else
    exit 1
fi
複製代碼

運行 C-Reduce 程序咱們能夠達到以下的簡化版本:

enum a {
    case b, c, d
    func e() -> f {
    	switch self {
    	case .b:
    	    0
    	case .c:
    	    0
    	case .d:
    	    0
    	}
    	return f()
    }
}

class f{}
複製代碼

深刻解析該編譯錯誤超出了本文的範圍,但若是咱們須要對其進行修復時,該簡化版本顯然更方便。咱們獲得了一個至關簡單的測試用例。 咱們還能夠推斷出 Swift 語句和類的實例化之間存在一些交互,不然 C-Reduce 可能會刪除其中一個。這爲編譯器致使該崩潰的緣由提供了一些很是好的提示。

總結

測試示例的盲約精簡併非一種多複雜的調試技術,可是自動化讓其變的更爲有用高效。C-Reduce 能夠做爲你調試工具箱的一個很好補充。它並不適用全部場景,可是它在面對有些問題時可以帶來不小的幫助。雖然在須要與多文件測試用例一塊兒工做時可能存在一些困難,但檢驗腳本可以解決了該問題。另外,對於 Swift 這類其餘語言來講 C-Reduce 也是開箱即用的,而不只僅只能在 C 語言中發揮功效,因此不要由於你使用的語言不是 C 而放棄它。

今天內容到此爲止。下次我還會帶來與編程和代碼相關的新內容。固然你也能夠將你感興趣的話題 發送給我

本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 swift.gg

相關文章
相關標籤/搜索