GCC-based embedded program instrumentation technology
2026-04-06 06:21:34··#1
Abstract In software testing, widely used dynamic testing methods such as coverage testing, fault injection, and performance analysis are all based on program instrumentation technology. This paper introduces a new method for implementing program instrumentation by analyzing and modifying the GCC compiler tool. This method has advantages such as batch automatic instrumentation, tight integration of instrumentation and compilation linking, and wide applicability to various languages. Finally, it specifically discusses how to implement program instrumentation in ARM embedded programs and provides the source code for modifying GCC. Keywords GCC, Program Instrumentation, ARM Embedded Programs Introduction The concept of program instrumentation was first proposed by Professor J. G. Huang. It is a method of obtaining control flow and data flow information by inserting operations (called "probes") into the program under test, thereby achieving the testing objective. In dynamic software testing, program instrumentation is a fundamental and widely used testing method, and it is the basic technology for coverage testing, software fault injection, and dynamic performance analysis. GCC (GNU Compiler Collection) is a highly optimized, highly portable, and widely used compiler system. It can handle multiple languages, including C/C++, Fortran, Java, and Pascal front-ends, and its back-end supports almost all processor architectures. As open-source software, GCC can be freely modified and used; with the addition of instrumentation modules, corresponding test code can be inserted into any language supported by GCC (this section only introduces the C language instrumentation module). This article will describe in detail how to modify GCC so that when compiling each C function, it passes each formal parameter along with the function name to a specified function. The return value of this specified function is assigned to the original formal parameters, thus allowing manual control over the actual values of each parameter of the instrumented function, thereby completing tests under various rules. 1. GCC Compilation Process Analysis The compiler's job is to translate source code (usually written in a high-level language) into object code (usually low-level object code or machine language). In modern compiler implementations, this work is generally implemented in two stages: In the first stage, the compiler's front-end receives the input source code and obtains a certain intermediate representation of the source program through lexical, syntactic, and semantic analysis. In the second stage, the compiler's backend optimizes the intermediate representation generated by the frontend and ultimately generates code that can run on the target machine. The GCC compiler processes the preprocessed input source file as a function. Based on the parser generated by GNU Bison (a more powerful grammar analysis tool similar to YACC), the frontend performs syntax and semantic analysis, builds a syntax tree, and converts it into intermediate code. Internally, GCC uses a hardware-platform-independent language that abstracts the actual architecture; this intermediate language is RTL (Register Translator Language). By modifying the RTL of the source program, the source program can be changed or deleted, including inserting necessary code. The GCC backend processes this and ultimately outputs assembly code corresponding to the hardware platform, allowing the source program to be instrumented without manual modification. The GCC entry point, the `main` function, is located in the file `main.c`. This function is very simple, containing only one statement that directly calls the `toplev_main` function. The `toplev_main` function is defined in the `toplev.c` file; we will only focus on the source code related to compilation below, ignoring the rest for now. The most important part of `toplev_main` is the call to the `do_compile` function, which, as its name suggests, performs the compilation work; after this, `toplev_main` returns. The `dD_compile` function is also defined in `tokv.c`, where the actual compilation work is done by calling the `compile_file` function. The `compile_file` function ultimately calls a hook function to parse the entire input file: `(*lang_hooks.parse_file)(set_yydebug);` Here, `lang_hooks` is a global variable, assigned different values by different language front-ends. For C, this statement is equivalent to calling the `c_common_parse_file` function in `c-opts.c`. `c_common_parse_file` calls the `c_parse_file` function in `c-parse.c`; this function in turn calls the `yyparse` function in the same file, which is responsible for parsing the C source file and converting it into a special syntax tree structure. This function is automatically generated by GNU bison when converting YACC to C, so this code is relatively difficult to read, but we are not concerned with the details of the syntax analysis. After analyzing the function body, the established tree structure is used to generate RTL, which is then optimized and finally output as assembly code. This completes the compilation of the C function, which is accomplished by yyparse calling the finish_function function. The most important function in finish_function is tree_rest_of_compilation (defined in tree_optimize.c), which is the function that actually implements the above functionality. To illustrate its specific actions, we have simplified this function, retaining only the key parts. After expanding the function into RTL form, the rest_of_compilation function is called to output the RTL as assembly code. Thus, a clear function call path during GCC compilation is obtained, as shown in Table 1. 2. GCC-based Program Instrumentation Technology According to the requirements of instrumentation testing, a hook function needs to be called for each parameter at the beginning of the function, and the parameter values are updated with the return value of the hook function. Simultaneously, the name of the instrumented function is pushed onto the function's local stack as an anonymous local variable, used only to be passed to the hook function. From the source code of the `tree_rest_of_compilation` function listed above, we know that the function responsible for establishing the parameters and return value of the compiled function is `expand_function_start`, defined in the file `function.c`. The function handling function parameters and return value within `expand_function_start` is `assign_parms`, which requires special attention. Below is a simplified pseudocode of this function: The bold italicized parts are the added code. Before the `for` loop, the name of the currently compiled function is obtained (see position ① in the source code); however, it cannot be output to the function's RTL chain yet because the local stack is only fully established after all parameters have been passed. Before the end of the `for` loop, a copy of the function parameters is recorded (see ②), and finally, the `insert_function_name_local` function is called to insert the current function name into the local stack and correct the stack pointer (see ③). After these modifications, all the information required for instrumentation is obtained, including the RTX representations of the function parameters and function name. GCC organizes the RTX representations generated after function compilation into a linked list, and finally outputs this RTX linked list as assembly code for the backend platform all at once. The `rest_of_compilation` function performs this task. Therefore, our RTX is inserted before calling `rest_of_compilation`, and the instrumentation is finally completed by the `inject_rtl` function. Below is the main code of `inject_rtl`: 3 APCS and Program Instrumentation Implementation Compilers must use a unified method to compile function definitions and calling procedures to ensure that functions written in different languages can call each other. The specification of these details is called the "Procedure Call Standard". The ARM architecture defines its own function call standard—the ARM Procedure Call Standard (APCS). Although APCS is not mandatory, implementing APCS is not difficult and provides the benefit of unified binary compatibility, so most compilers implement APCS, including GCC. The definition of function parameter passing in APCS is as follows: ◇The first four integer arguments (or fewer) are loaded into r0~r3. ◇The first four floating-point arguments (or fewer) are loaded into f0~f3. ◇ If the parameter is a double word (8 bytes), it must be placed starting from an even-numbered register. ◇ If a parameter cannot fit entirely into a register, the excess is copied onto the stack. Any other arguments (if any) are stored in memory, pointed to by the word immediately preceding the sp value upon entering the function. In other words, the remaining arguments are pushed onto the stack. Therefore, for simplicity, it is best to define functions that accept four or fewer integer arguments. The insertion function described in this article has only two integer parameters, so when calling it, only the two arguments ro and rl need to be passed respectively. GCC provides the emit_library_call function to generate the RTL code for the function call, and GCC will generate the correct function call assembly code according to APCS. The function is defined in calls.c, and its prototype is: After inserting the required function, the return value needs to be assigned to the corresponding parameter of the instrumented function. The following is the complete code of the insertion function insert_parms_test_function: 4 Example To facilitate checking the instrumentation effect, a simple C program is compiled using a modified GCC. This program is a standalone function foo that accepts two integer parameters. The specific code is as follows: From the assembly code output by GCC, we can see that both parameters of the `foo` function are updated by the hook function `pt_hook_partns`. Within the `pt_hook_partns` function, different boundary values can be returned according to the test algorithm, thus achieving the testing purpose. Following this method, an actual program, after instrumentation, runs smoothly on the ARM simulator and achieves the expected test results. Conclusion This article discusses in detail the implementation method of modifying GCC to add instrumentation functionality. Following this approach, dynamic parameter boundary testing of an embedded system based on an ARM7 chip was successfully implemented, achieving the expected results. The instrumentation function described in this article is relatively simple, without distinguishing parameter types; all parameters are processed as a single word. The next step is to subdivide different parameter types and instrument different processing functions. As a general instrumentation method, based on this, by identifying different instrumentation points and instrumenting different functions, functions requiring instrumentation technology as a foundation can be implemented, such as function call stack checking, program coverage testing, and obtaining the actual execution time of functions.