Assert Rewriting - A GCC plugin - part 1

栏目: IT技术 · 发布时间: 4年前

内容简介:This article tells the story of how I got into developing a GCC plugin that rewrites C asserts, in attempt to mimicThe plugin itself is availableA few weeks ago I started working on a small side project in C, nothing too fancy. As with all new codebases, i

This article tells the story of how I got into developing a GCC plugin that rewrites C asserts, in attempt to mimic py.test ’s assert messages. It’s also a beginner’s guide to GCC plugin development.

The plugin itself is available here .

Poor man’s asserts

A few weeks ago I started working on a small side project in C, nothing too fancy. As with all new codebases, it was prone to many structural changes, API refactors etc. So I tried to add as many asserts as possible, and also wrote some basic C tests. When writing lots of new code, I try to have tests written ASAP, to and fill the code with asserts since they help in quickly catching problems common to a developing codebase (like missed refactors / renames, misused APIs, …).

One thing I dislike about those asserts it that watching them fail usually doesn’t provide you with all the information necessary to fix the problem. A common assert like:

assert(n == 7);

will fail with:

main: main.c:6: main: Assertion `n == 7' failed.

telling you that n wasn’t 7 , but not telling you what was the value n , which might be required to identify the reason of failure.

The problem gets worse when you have more complex expressions, like && , || or function calls. So when an assert fails, I usually have to replace it with a printf of all expressions, recompile and rerun the failing code just so I could know what the problem was.

Back in that side project, after a session of about 10 “replace-assert-with-printf-then-rerun” changes, I thought there must be something I could do to improve it.

Then py.test popped into my mind. py.test is one the well-known Python test frameworks, and one of the features I like in it is that you write your tests logic using plain assert s, but unlike the standard Python assert failure (which raises an empty AssertionError ), py.test prints a very elaborated assert failure message. For example, the following assert:

assert min([1, 2, 3]) - 5 + min(1, 2, 3) == max([5, 6, 5])

will fail with this exception:

>       assert min([1, 2, 3]) - 5 + min(1, 2, 3) == max([5, 6, 5])
E       assert ((1 - 5) + 1) == 6
E        +  where 1 = min([1, 2, 3])
E        +  and   1 = min(1, 2, 3)
E        +  and   6 = max([5, 6, 5])

That’s very cool. It would have been very useful if I had something similar in C. I decided to try and build that something, for my C code.

Adding introspection to asserts

I searched online and found this read, which gives a brief introduction on assert rewriting in py.test . The author (who wrote the feature in py.test ) explains how subexpression information can be displayed by modifying the AST (Abstract Syntax Tree) of your test code.

If you’re not sure what’s AST, this image from Wikipedia will help. It shows the AST of a simple piece of code, written in its description.

Python, being a dynamic language, allows you to modify the AST of modules during the loading process. py.test uses this to dismantle your assert s into smaller code pieces that are executed one by one. If any of them fails, the generated code is ready to print information about the exact point of failure.

Let’s see a simple, manual “assert rewriting” example. To gain introspection into:

assert n == 8 and myfunc() == 12

we can rewrite it as:

if n == 8:
    _tmp = myfunc()
    if _tmp == 12:
        # all good! the assert passed
        pass
    else:
        raise AssertionError("{0} != 12 (myfunc() = {0})".format(_tmp))
else:
    raise AssertionError("{0} != 8 (n = {0})".format(n))

As you can see, even for simple expressions it quickly becomes complex. For example, if any of the subexpressions has side-effects, it must be stored in a temporary variable (like _tmp ) so we don’t invoke those side-effects multiple times. In the real world it’s gets even more complicated ;)

Lucky for us, this conversion is entirely automatic! py.test takes care of everything, and we get to write human-readable assert expressions.

Back to C

C is not a dynamic language. I can’t modify the AST of my program code in runtime. The compiler leaves you with machine instructions, it’s hard to extract accurate information about the original source code by messing with them in runtime.

Test frameworks in C need to provide useful failure message when test asserts fail to help you with debugging your tests, so they must do something more fancy than plain C assert s.

What’s common for them is to provide you a set of assert functions, suitable for all comparison types you may need. For example, the libraryCheck has the following set of functions/macros (excerpt from src/check.in.h )

...
#define     ck_assert_int_eq(X, Y)   _ck_assert_int(X, ==, Y)
...
#define     ck_assert_int_ne(X, Y)   _ck_assert_int(X, !=, Y)
...
#define     ck_assert_int_lt(X, Y)   _ck_assert_int(X, <, Y)
...
#define     ck_assert_int_le(X, Y)   _ck_assert_int(X, <=, Y)
...

As you can see, you don’t write the expression X == Y but instead use ck_assert_int_eq with the arguments separated. _ck_assert_int can use the separated arguments to create a proper assert message:

#define _ck_assert_int(X, OP, Y) do { \
  intmax_t _ck_x = (X); \
  intmax_t _ck_y = (Y); \
  ck_assert_msg(_ck_x OP _ck_y, "Assertion '%s' failed: %s == %jd, %s == %jd", #X" "#OP" "#Y, #X, _ck_x, #Y, _ck_y); \
} while (0)

This produces useful messages, but the downside is that you have to remember all the ck_assert_* functions (thelist is long), and write unintuitive code instead of a simple == . If ck_assert_int_eq would accept the expression X == Y instead, it won’t have any introspection on the expression - it could only operate on the value of it.

This “problem” isn’t affecting Check only - in C, and in most other compiled languages, you don’t have access to the information required to parse your code’s own expression trees. You’ll need to be a part of the compilation process in order to get your hands on the AST. To be a part of the compilation process means, in most compiled languages, that you are required to be a part of the compiler, or a plugin of it.

Actually, Rust is the only language I know that lets your code intervene in it’s own compilation process, via procedural macros.

We’re in C, so… a GCC plugin it is.

Assert Rewriting - A GCC plugin - part 1

GCC plugins intro

GCC provides an API for plugins . Plugins are programs built once, then loaded when invoking GCC. The can modify its behvaior, add new features, etc. More specifically, your plugin can register callbacks which GCC will call in different steps during compilation, allowing the plugin to mess around with GCC’s internal objects.

The GCC wiki has a short page with some background on plugins. It also lists some existing plugins, although the list isn’t so long. To get the hang of what plugins are capable of, here’s a brief on one of the GCC plugins the Linux kernel uses for its own build: named randstruct , this plugin randomizes the layout of structs during the kernel build process, so different kernel builds use different layout for their internal structs. This is an obfuscation, done to help in exploit mitigation. No way you could do that without a plugin - you’d have to change your code manually.

Into GCC

Here’s what I had in mind: I’ll write a plugin that “walks” over functions’ code, identifies calls to assert and rewrites them with code that’ll do all those nice printings. I didn’t stop to think how exactly this “code” is gonna work… I didn’t even bother looking in py.test ’s implementation, though I wanted the plugin to produce similar messages.

So, GCC plugins. I had some experience writing those; a few months ago I wrote struct_layout . That plugin processes struct definitions as GCC encounters them, then dumps them in a specialized format.

I started by copying the plugin boilerplate from it. A plugin is basically a shared object which exports a plugin_init function. GCC calls it when the plugin is loaded, and your plugin can register callbacks for various compilation events using register_callback .

struct_layout registered on the event PLUGIN_FINISH_DECL , which is generated after finishing parsing a declaration . This time we need an event related to code parsing & AST. A quick look in plugin.def yields PLUGIN_PRE_GENERICIZE which is documented as allows to see low level AST in C and C++ frontends . Perfect!

The required prototype for the callback is void my_callback(void *event_data, void *user_data) . user_data is a “private data” pointer (we can pass to register_callback and get it here), and event_data is the data itself: a tree object.

GCC trees

From the GCC docs :

The central data structure used by the internal representation is the tree.

Short enough, but it says a lot. Trees are a very central data structure, literally everything is represented by a tree object, and those can wield different types and properties. That “internal representation”, made of trees, creates the AST.

This article talks about the GCC internal representation called GENERIC. There’s another internal representation in GCC, named GIMPLE, but we won’t cover it here.

The AST is composed of multiple nodes; They can represent statements, declarations, types, … you get my point. We’re interested in reading functions. From my previous work on plugins, I remembered there’s a type of tree for representing functions: it’s called FUNCTION_DECL .

At this point I didn’t know what’s inside this FUNCTION_DECL tree. Plugin development guides are scarce, but after a bit of searching I found a nice guide giving concrete examples of AST nodes in GCC . I suggest you take a quick stop and read it (it’s short). At least take a look at the fancy diagrams.

Another useful document is GCC GENERIC docs where you can find a short paragraph on many (if not all?) tree types. I keept it open in one tab for the course of developemnt.

Plugin Basics

I think we know enough to start, let’s get some code running. Start by setting up your environment for plugin development. Some distros require the installation of a “GCC plugin dev” package (e.g Ubuntu), in others you get it in your standard GCC installation (e.g Arch)

For Ubutnu:

# You'll need gcc-X-plugin-dev, where X is your GCC major. check it with gcc --version.
# For Ubuntu 18.04 that will be:
$ sudo apt-get install gcc-7-plugin-dev

Let’s compile the empty plugin skeleton I described above. A minimal file is:

#include <stdio.h>

#include <gcc-plugin.h>
#include <tree.h>
#include <plugin-version.h>

int plugin_is_GPL_compatible; // must be defined & exported for the plugin to be loaded

static void pre_genericize_callback(void *event_data, void *user_data) {
    tree t = (tree)event_data;

    // ...
}

int plugin_init(struct plugin_name_args *plugin_info, struct plugin_gcc_version *version) {
    printf("Loaded! compiled for GCC %s\n", gcc_version.basever);
    register_callback(plugin_info->base_name, PLUGIN_PRE_GENERICIZE, pre_genericize_callback, NULL);

    return 0;
}

Write it to basic.c and build it:

$ g++ -g -I`gcc -print-file-name=plugin`/include -fpic -shared -o basic.so basic.c

The gcc -print-file-name=plugin prints the location where plugin headers are installed. We also compile with -fpic -shared to produce a shared library.

Now we’ll try to load it.

$ touch empty.c
$ gcc -fplugin=./basic.so empty.c -c
Loaded! compiled for GCC 9.1.0

Great. Now what do we do with that tree we have in our callback? It’s time to meet the single-handedly most useful debugging function you can use while developing for GCC: debug_tree . It accepts a tree object and dumps it recursively (to some depth), printing its type, its fields and descendants.

Add #include <print-tree.h> to the top, and change our callback function to this:

static void pre_genericize_callback(void *event_data, void *user_data) {
    tree t = (tree)event_data;

    debug_tree(t);
}

We’ll write a dummy function and compile it with the plugin active. -fplugin=/path/to/plugin.so tells GCC to load our plugin.

# start by recompiling the plugin
$ g++ -g -I`gcc -print-file-name=plugin`/include -fpic -shared -o basic.so basic.c
$ echo 'int dummy(int n) { return n + 5; }' > dummy.c
$ gcc -fplugin=./basic.so dummy.c -c

You should see something like::

<function_decl 0x7f13df35be00 dummy
    type <function_type 0x7f13df263bd0
        type <integer_type 0x7f13df2525e8 int public SI
...

This is the AST node representing our function. A few words on the format:

  • Nodes (trees) begin with a < and end with a > .
  • Node type follows the < ; Here we have function_decl , function_type and integer_type . Those types are themselves trees.
  • After the node type comes the node object addres in memory. It doesn’t convery much info, but it’s useful when comparing nodes because it tells you when two nodes are really equal (because they are the same object).
  • After the address comes an optional string describing the node. For declarations, it’s the name (our function_decl is named dummy ). For types (such as integer_type ) it’s the type name (here int ).
  • The string before an opening < is the “name” of the field in the containing node. Here our function_decl has type of function_type , and function_type has type of integer_type .

Functions in the AST

I don’t see anything in the previous print about the function code, though! One of the diagrams in AST representation in GCC will be of help. It shows functions have a DECL_SAVED_TREE field, and gives usage example:

tree fnBody = DECL_SAVED_TREE (fnDecl);

The body is a tree itself, of course. So we can alter our debug_tree(t) to debug_tree(DECL_SAVED_TREE(t)) .

Recompile the plugin, the recompile dummy.c with the plugin:

<bind_expr 0x7fab614c3a80
    type <void_type 0x7fab6139bf18 void VOID
        align 8 symtab 0 alias set -1 canonical type 0x7fab6139bf18
        pointer_to_this <pointer_type 0x7fab613a30a8>>
    side-effects
    body <return_expr 0x7fab614b1d40 type <void_type 0x7fab6139bf18 void>
        side-effects
        arg 0 <modify_expr 0x7fab614a0b90 type <integer_type 0x7fab6139b5e8 int>
            side-effects arg 0 <result_decl 0x7fab613911e0 D.1794>
            arg 1 <plus_expr 0x7fab614a0b68 type <integer_type 0x7fab6139b5e8 int>
                arg 0 <parm_decl 0x7fab614c4000 n>
                arg 1 <integer_cst 0x7fab613a03f0 constant 5>
                dummy.c:1:29 start: dummy.c:1:27 finish: dummy.c:1:31>
            dummy.c:1:29 start: dummy.c:1:27 finish: dummy.c:1:31>
        dummy.c:1:29 start: dummy.c:1:27 finish: dummy.c:1:31>
    block <block 0x7fab614a97e0 used
        supercontext <function_decl 0x7fab614a4e00 dummy type <function_type 0x7fab613acbd0>
            public static QI file dummy.c line 1 col 5 align 8 initial <block 0x7fab614a97e0> result <result_decl 0x7fab613911e0 D.1794> arguments <parm_decl 0x7fab614c4000 n>
            struct-function 0x7fab614c5000>>
    dummy.c:1:18 start: dummy.c:1:18 finish: dummy.c:1:18>

Lots of prints. Don’t get scared though, read and “parse” it slowly because everything here is logical. The function body is a bind_expr . What’s that? Head to the GENERIC docs and under Statements -> Blocks you’ll find a short explanation on BIND_EXPR :

Block scopes and the variables they declare in GENERIC are expressed using the BIND_EXPR code, ...

So a BIND_EXPR is just… Code and variables enclosed in { } . Very simple.

Note: debug_tree prints type names like bind_expr and field names like body in lowercase, but in the source code these are written in uppercase. From now on, I’ll be using the source notation, uppercase.

We can already see what the body contains, but let’s try getting the inner body tree first. Most tree types have special macros to access their fields; those can be found in gcc/tree.h . Time to grab a copy of the source!

When working with the source code of a project, I like having its Git available, so I can look up in the history, read commit messages, find out when a change was included into the mainline, etc. So I suggest you to clone GCC’s code: git clone https://github.com/gcc-mirror/gcc.git . You can also get the latest version only if you’re hasty: git clone --depth 1 https://github.com/gcc-mirror/gcc.git

By the way, we already have a local copy of tree.h and other API headers; We got them when we installed the plugin dev package. Nonetheless, having the full source code (including the implementation of things) is useful.

Open gcc/tree.h and search for “bind”. If possible, when searching for symbols, use symbolic search in this file since it’s utterly huge. I use Sublime, so Ctrl+R “bind” led me to BIND_EXPR_VARS , BIND_EXPR_BODY and BIND_EXPR_BLOCKS .

BIND_EXPR_BODY sounds like it. Let’s debug_tree it: debug_tree(BIND_EXPR_BODY(DECL_SAVED_TREE(t))) .

<return_expr 0x7fc48ba6fd40
    type <void_type 0x7fc48b959f18 void VOID
        align 8 symtab 0 alias set -1 canonical type 0x7fc48b959f18
        pointer_to_this <pointer_type 0x7fc48b9610a8>>
    side-effects
    arg 0 <modify_expr 0x7fc48ba5eb90
        type <integer_type 0x7fc48b9595e8 int public SI
            size <integer_cst 0x7fc48b941f18 constant 32>
            unit size <integer_cst 0x7fc48b941f30 constant 4>
            align 32 symtab 0 alias set -1 canonical type 0x7fc48b9595e8 precision 32 min <integer_cst 0x7fc48b941ed0 -2147483648> max <integer_cst 0x7fc48b941ee8 2147483647>
            pointer_to_this <pointer_type 0x7fc48b961a80>>
        side-effects
        arg 0 <result_decl 0x7fc48b94f1e0 D.1794 type <integer_type 0x7fc48b9595e8 int>
            ignored SI file dummy.c line 1 col 5 size <integer_cst 0x7fc48b941f18 32> unit size <integer_cst 0x7fc48b941f30 4>
            align 32 context <function_decl 0x7fc48ba62e00 dummy>>
        arg 1 <plus_expr 0x7fc48ba5eb68 type <integer_type 0x7fc48b9595e8 int>
            arg 0 <parm_decl 0x7fc48ba82000 n>
            arg 1 <integer_cst 0x7fc48b95e3f0 constant 5>
            dummy.c:1:29 start: dummy.c:1:27 finish: dummy.c:1:31>
        dummy.c:1:29 start: dummy.c:1:27 finish: dummy.c:1:31>
    dummy.c:1:29 start: dummy.c:1:27 finish: dummy.c:1:31>

Walk it top down:

  • RETURN_EXPR is explained in the GENERIC docs, Statements -> Basic Statements. Its first operand is the value to return, in our case a MODIFY_EXPR .
  • MODIFY_EXPR is explained under Expressions -> Unary and Binary Expressions: A MODIFY_EXPR , simply, is an assignment (includes assignments like += as well as = ). Here its first operand is RESULT_DECL , which is a tree representing the return value of the current function - a value assigned to RESULT_DECL indicates it should be returned.
  • The second operand of the MODIFY_EXPR is a PLUS_EXPR : a simple + between its 2 operands.
  • The operands to our PLUS_EXPR are a PARM_DECL (represents a parameter to a function) and an INTEGER_CST which represents integer cosntants. The details displayed by debug_tree show us the PARM_DECL is named n , and the integer constant is 5 .

Okay, we know how to read and parse out simple AST nodes! Now comes the question - is it possible to overwrite them during compilation? (Well, ealier I did say it’s possible… but let’s see how)

AST modifications

To replace a node with another one, we’ll need to create a new node first. Functions constructing new trees will begin with build , and tree.h has plenty of them. Most are not documented in tree.h itself but instead in their definition. My suggestion is that you try searching tree.h for keywords of what you need, it works quite well.

For our modification test - let’s modify the AST to make dummy return n + 10 instead of n + 5 . So we need to replace the INTEGER_CST with another one, holding the value of 10.

Symbolic searching for “build int” yields build_int_cst . It accepts a “type” tree, and a value for the new const. The “type” may be the tree representing the C type int , the C type long , etc. All those common types are declared in tree.h . We will use integer_type_node for a plain int .

In tree.h we’ll also find TREE_OPERAND(NODE, I) , which accesses the I’th operand of a node. This way we can walk down the expression tree until we get to the node we wish to replace.

static void pre_genericize_callback(void *event_data, void *user_data) {
    tree t = (tree)event_data;

    tree body = BIND_EXPR_BODY(DECL_SAVED_TREE(t));
    tree modify = TREE_OPERAND(body, 0);
    tree plus = TREE_OPERAND(modify, 1);
    // replace it! (TREE_OPERAND's result is an lvalue)
    TREE_OPERAND(plus, 1) = build_int_cst(integer_type_node, 10);
}

Recompile the plugin, recompile dummy.c with the plugin, and write another simple file main.c :

#include <stdio.h>

int dummy(int n);

int main(void) {
    printf("%d\n", dummy(1));
    return 0;
}

Build (this time without the plugin):

$ gcc main.c dummy.o -o main  # note: use the dummy.o compiled with the plugin, not dummy.c!

And…

$ ./main
11

Worked! Hooray.

You can compile dummy.c again without the plugin to see the difference:

$ gcc main.c dummy.c -o main  # dummy.c this time
$ ./main
6

And that concludes it for this post.

I actually intended to focus on the assert rewriting part, but ended up talking about GCC for the most part. I’m happy it came out this way, though - by this guide I wish to provide smoother entry to plugin development for everyone. During my career as a developer I have spent many hours reading GCC’s user docs, searching for the right compiler flag for a task, etc. Gaining insight in the internals, being able to extend C & GCC with features I couldn’t have earlier - well, it was very nice, and I think every developer should try it sometime :)

In the next part, we’ll talk about the assert rewriting logic itself, and how to embed it in the AST via the plugin. We’ll also dive deeper into GCC and GENERIC representation.

GCC Plugin Development Resources

I collected here the references I gave through the article. Many we useful for me during this project, some I found only afterwards. Anyway, all are useful and thus I put them here together so future plugin developers can use them.

  1. AST representation in GCC - AST in GCC trees.
  2. GCC GENERIC trees - the official docs for tree types.
  3. Understanding Trees - in GCC wiki, short but contains useful examples.
  4. Playing with GCC plugins - simple plugin that logs structs’ sizes. Insipired me to try plugins.
  5. A simple plugin for GCC - implements the warn_unused_result attribute correctly for C++. That guide works with GIMPLE, not with GENERIC as I did here, but some parts are common and nonetheless it’s an interesting read.
  6. GCC tiny , by the same author of the previous entry - writing a new GCC front end for a simple language. Based on GENERIC. Very comprehensive.
  7. This guide itself :) And its future parts, in days to come…

以上所述就是小编给大家介绍的《Assert Rewriting - A GCC plugin - part 1》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

千夫所指

千夫所指

乔恩·罗森 / 王岑卉 / 九州出版社 / 2016-10-1 / CNY 42.80

编辑推荐: 《乌合之众》是为了跪舔权贵?《普通心理学》实验存在重大漏洞?《引爆点》的理论都是瞎掰的?社交网络时代《1984》预言的“老大哥”是否已经变成事实? 《纽约时报》年度十佳书 《GQ》杂志年度十佳书 《卫报》年度十佳书 《泰晤士报》年度十佳书 《经济学人》年度重推! 黑天鹅年度重点图书! 《乌合之众》是为了迎合权贵?《普通心理学》实验存在重大......一起来看看 《千夫所指》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具