By Mike Ash, author, 2018-06-29 Proofreading: PMST, NUMbbbbb; Finalized: Forelax

Debugging complex problems is not easy in itself, and can be very difficult without sufficient context and general direction. So simplifying code to narrow the scope of debugging becomes a common behavior. But it is far easier to exploit the computer’s strengths by performing automated processing than by tedious manual simplification. This is where C-Reduce comes in. It automatically simplifies the original code and outputs a simplified, debug-friendly version. Let’s look at how to use this automation.

An overview of the

The C-Reduce code is based on two main ideas.

First, C-Reduce converts some original code into a simplified version by, for example, removing the relevant lines of code or renaming the token to a shorter version.

Secondly, the simplified results are checked and tested. The code simplification above is mindless and often results in simplified versions with no errors to trace or even no compilation at all. So when using C-Reduce, you need a script in addition to the original code that tests whether the simplified operation meets certain “expectations.” The standard of “expectation” is set by us according to the actual situation. For example, if you want to locate a bug, “expected” means that the simplified version contains errors consistent with the original code. You can write any “expected” standard you want with a script that c-Reduce uses to ensure that the simplified version conforms to the predefined behavior.

The installation

The c-Reduce program has many dependencies and is complex to install. Thanks to Homebrew, we can simply type the following command:

brew install creduce
Copy the code

If you want to install it manually, you can refer to this installation guide.

Simple example

It is difficult to come up with a small example code to explain C-Reduce, because its main purpose is to simplify a small example from a large program. Here’s a simple C program code THAT I did my best to come up with that produces some incomprehensible compile warnings.

$ 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.
Copy the code

We know from the warning that there is something wrong with the struct and main code! As to what the specific problem is, we can analyze it in detail in the simplified version.

C-reduce can simplify programs much more easily than we can imagine. So in order to control the reduced behavior of C-Reduce and ensure that the simplified operation meets certain expectations, we will write a small shell script that compiles the code and checks for warnings. In this script we need to match compilation warnings and reject any form of compilation errors, and we also need to make sure that the output file contains struct Stuff. The script is detailed as follows:

#! /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
Copy the code

First, we compile the simplified code and redirect the output to output.txt. If the output file contains any “error” words, exit immediately and return status code 1. Otherwise the script will continue to check if the output text contains specific warning information and the text struct Stuff. If grep matches both conditions successfully, status code 0 is returned. Otherwise, exit and return status code 1. Status code 0 indicates that the code meets expectations and status code 1 indicates that the simplified code does not meet expectations and needs to be simplified again.

Let’s run C-Reduce to see what happens:

$ creduce interestingness.sh test.c 
===< 4907 >===
running 3 interestingness tests inThe parallel = = = < pass_includes: : 0 > = = = (111 bytes) 14.6%,... 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() {}Copy the code

We end up with a simplified version that meets our expectations and overwrites the original code file. So be aware of this when using C-Reduce! You must run C-Reduce in a copy of the code to simplify operations, otherwise irreversible changes may be made to the original code.

This simplified version successfully exposes code problems: forgetting to add a semicolon to the end of the struct Stuff type declaration, and not specifying the return type of the main function. This causes the compiler to incorrectly treat struct Stuff as a return type. Main must return an int, so the compiler issues a warning.

The Xcode project

C-reduce is great for simplifying a single file, but what about more complex scenarios? Most of us have multiple Xcode projects, so how can we simplify one Xcode project?

Given how C-Reduce works, simplifying Xcode projects is not easy. It copies the files that need to be simplified to a directory and then runs the script. While this makes it possible to run multiple simplification tasks simultaneously, it may not be possible to simplify if you need other dependencies to make it work. Fortunately, you can run various commands in scripts, so you can solve this problem by copying the rest of the project to a temporary directory.

I used Xcode to create a standard Objective-C Cocoa application and made the following changes to Appdelegate.m:

#import "AppDelegate.h"

@interface AppDelegate(a){
    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
Copy the code

This code will crash the app at startup:

* 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
Copy the code

This is not very useful call stack information. Although we can trace the problem through debugging, here we try to use C-Reduce to locate the problem.

The c-Reduce definition here is expected to cover a lot more. First we need to set a timeout for the application to run. We do crash capture at run time, and if no crash occurs we keep the application running until timeout processing is triggered to exit. Here is a snippet of Perl script code that is widely available on the web:

function timeout() { perl -e 'alarm shift; exec @ARGV' "$@"; }
Copy the code

Next we need to copy the project file:

cp -a ~/Development/creduce-examples/Crasher .
Copy the code

Then copy the modified appdelegate. m file to the appropriate path. Note: C-reduce will copy the file back if it finds a suitable simplified version, so be sure to use cp instead of mv here. Using mv results in a strange fatal error.

cp AppDelegate.m Crasher/Crasher
Copy the code

Next we switch to the Crasher directory to execute the compile command and exit when an error occurs.

cd Crasher
xcodebuild || exit 1
Copy the code

If the compilation succeeds, the application is run and the timeout is set. The build items are set up on my system, so the XcodeBuild command stores the build results in the local build directory. Because configurations may vary, you need to check yourself first. If you set the configuration to a shared build directory, add -n 1 to the command line to disable concurrent C-Reduce builds.

timeout 5 ./build/Release/Crasher.app/Contents/MacOS/Crasher
Copy the code

If the application crashes, a specific status code 139 is returned. In this case we need to convert it to status code 0, and in all other cases return status code 1.

if[$?-eq139];then
    exit 0
else
    exit 1
fi
Copy the code

Next, we run 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
Copy the code

We get an extremely lean code. Although C-Reduce does not remove the NSLog line, it does not appear to have caused the crash. So the only code that will crash here is a = NSInsetRect(a, 0, 10); This line of code. By checking the bank code function and use of the variables, we can find it using a NSRect and applicationDidFinishLaunching function into the types of variables and in fact is not that type.

- (void)applicationDidFinishLaunching:(NSNotification *)notification;
Copy the code

Therefore, the crash should be caused by an error caused by a type mismatch.

Because compiling a project takes longer than a single file and many test examples trigger timeout processing, c-Reduce in this example takes a long time to run. C-reduce writes the condensed file back to the original after each successful run, so you can use a text editor to keep the file open and view the changes. In addition, you can run the ^C command to end c-Reduce execution when appropriate, and you will get a partially reduced file. You can build on that later if you need to.

Swift

What if you use Swift and also have streamlining needs? Given the name, I thought C-Reduce was only for C (and maybe C++, as many tools are).

Fortunately, my instincts were wrong this time. C-reduce does have some c-specific validation tests, but most of them are language-neutral. If you can write validation tests in any language, C-Reduce can come in handy, although it may not be very efficient.

So let’s give it a try. I found a great test case at bugs.swift.org. However, the crash was only on Xcode9.3, which I had installed. Here is a simple modified version of the bug example:

import Foundation

func crash(a) {
    let blah = ProblematicEnum.problematicCase.problematicMethod()
    NSLog("\(blah)")}enum ProblematicEnum {
    case first, second, problematicCase

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

    	switch self {
    	case .first:
    	    someVariable = SomeClass(a)case .second:
    	    someVariable = SomeClass(a)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()
Copy the code

When we try to run the code with optimization enabled, we get the following results:

$ 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' whichcould not be resolved! .Copy the code

The corresponding verification script is:

swift -O test.swift
if[$?-eq134];then
    exit 0
else
    exit 1
fi
Copy the code

By running the C-Reduce program, we can achieve the following simplified version:

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

class f{}
Copy the code

Parsing this compilation error in depth is beyond the scope of this article, but this simplified version is obviously more convenient if we need to fix it. We have a fairly simple test case. We can also infer that there is some interaction between the Swift statement and the instantiation of the class, one of which might otherwise be removed by C-Reduce. This gives the compiler some very good hints as to what caused the crash.

conclusion

Blind reduction of test examples is not a sophisticated debugging technique, but automation makes it more useful and efficient. C-reduce can be a great addition to your debugging toolbox. It is not suitable for every situation, but it can be of great help when facing some problems. While there may be some difficulties when you need to work with multi-file test cases, validation scripts solve the problem. Also, C-Reduce works out of the box for other languages like Swift, not just C, so don’t give it up just because you’re not using C.

That’s all for today. Next time I’ll bring you something new about programming and code. Of course, you can also send me the topics you are interested in.

This article is translated by SwiftGG translation team and has been authorized to be translated by the authors. Please visit swift.gg for the latest articles.