Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

machine-check-avr

Machine-check-avr provides a description of a machine-code system based on the AVR ATmega328P microcontroller, with some caveats. The description was created by transcribing the appropriate parts of the instruction set manual and the non-automotive ATmega328P datasheet.

The Intel HEX files used to load the program into the microcontroller can be provided as an argument to machine-check-avr: see the docs for details. This enables machine-code verification using machine-check.

For an example discussed in a presentation about a previous version of machine-check (the program on the slides is simplified for visibility), consider the following program written in C that can be used for calibration using binary search:

#define F_CPU 1000000

#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
	
	DDRC |= 0x01;
	DDRD |= 0xFF;
    while (1) 
    {
		while ((PINC & 0x2) == 0) {}
		
		// signify we are calibrating
		PORTC |= 0x01;
		
		// start with MSB of calibration
		uint8_t search_bit = 7;
		uint8_t search_mask = (1 << search_bit);
		uint8_t search_val = search_mask;
		
		while (1) {
			// wait a bit
			_delay_us(10);
			// write the current search value
			PORTD = search_val;
			// wait a bit
			_delay_us(10);
			
			// get input value and compare it to desired
			uint8_t input_value = PINB;
			
			if ((input_value & 0x80) == 0) {
				// input value lower than desired -> we should lower the calibration value
				search_val &= ~search_mask;	
			}
			
			if (search_bit == 0) {
				// all bits have been set, stop
				break;
			}
			
			search_bit -= 1;
			// continue to next bit
			search_mask >>= 1;
			// update the search value with the next bit set
			search_val |= search_mask;
			
		}
		
		// calibration complete, stop signifying that we are calibrating
		PORTC &= ~0x01;
    }
}

We can use machine-check-avr to find a verify a property in the compiled machine-code version of the program. We are doing binary search and setting PORTD according to search_val. Let us consider that, at any point in the program, we should be able to use some sequence of inputs to coerce PORTD to be zero.

Machine-check-avr can be installed as an executable using cargo:

cargo install machine-check-avr
    Updating crates.io index
  Downloaded machine-check-avr v0.5.0
  Downloaded 1 crate (42.8 KB) in 1.03s
  Installing machine-check-avr v0.5.0
    Updating crates.io index
     Locking 74 packages to latest compatible versions
   Compiling proc-macro2 v1.0.95
   (...)
    Finished `release` profile [optimized] target(s) in 2m 12s
  Installing (...)\.cargo\bin\machine-check-avr.exe
   Installed package `machine-check-avr v0.5.0` (executable `machine-check-avr.exe`)

⚠️ Even with a Ryzen 9950X3D CPU, the compilation takes more than 2 minutes, and will take more (3-10 minutes) with a less powerful CPU. The most time is taken compiling machine-check-avr itself (which is essentially a single-core task) due to a high amount of auto-generated code. Fortunately, it is then possible to use machine-check-avr normally without recompilation (unless you wish to reinstall it with other features, such as GUI support).

The Intel HEX file obtained via compilation (that we can use to program the ATmega328P or provide to machine-check-avr) is not very self-explanatory (let us call it calibration_original.hex):

:100000000C9434000C943E000C943E000C943E0082
:100010000C943E000C943E000C943E000C943E0068
:100020000C943E000C943E000C943E000C943E0058
:100030000C943E000C943E000C943E000C943E0048
:100040000C943E000C943E000C943E000C943E0038
:100050000C943E000C943E000C943E000C943E0028
:100060000C943E000C943E0011241FBECFEFD8E04C
:10007000DEBFCDBF0E9440000C9466000C940000CF
:1000800087B1816087B98AB18FEF8AB9319BFECF82
:1000900088B1816088B980E890E827E033E03A953C
:1000A000F1F700008BB933E03A95F1F700001F99A2
:1000B00003C0392F30958323222321F021509695B8
:1000C000892BECCF88B18E7F88B9E0CFF894FFCF31
:00000001FF

We can try to verify the property, which we think should hold:

$ machine-check-avr --system-hex-file (...)\calibration_original.hex --property "AG![EF![PORTD == 0]]"
[2025-06-15T15:16:52Z INFO  machine_check] Starting verification.
[2025-06-15T15:16:52Z INFO  machine_check::verify] Verifying the inherent property first.
[2025-06-15T15:17:07Z INFO  machine_check::verify] The inherent property holds, proceeding to the given property.
[2025-06-15T15:17:07Z INFO  machine_check::verify] Verifying the given property.
[2025-06-15T15:17:07Z INFO  machine_check] Verification ended.
+----------------------------------+
|      Result: DOES NOT HOLD       |
+----------------------------------+
|  Refinements:               513  |
|  Generated states:        14090  |
|  Final states:            13059  |
|  Generated transitions:   14603  |
|  Final transitions:       13573  |
+----------------------------------+

This is caused by a bug in the program.

The problem is that PORTD is not updated to search_val at the end of search before the loop breaks, so the lowest bit is always set to one after each calibration. Let us fix this:

(...)
			if (search_bit == 0) {
				// all bits have been set, stop
				
				// FIX BUG FOUND BY TOOL: write the end value
				PORTD = search_val;
				break;
			}
(...)

Again, the new hex file does not tell us much, although it contains as much information as the microcontroller needs to execute the program (let's call it calibration_fixed.hex):

:100000000C9434000C943E000C943E000C943E0082
:100010000C943E000C943E000C943E000C943E0068
:100020000C943E000C943E000C943E000C943E0058
:100030000C943E000C943E000C943E000C943E0048
:100040000C943E000C943E000C943E000C943E0038
:100050000C943E000C943E000C943E000C943E0028
:100060000C943E000C943E0011241FBECFEFD8E04C
:10007000DEBFCDBF0E9440000C9467000C940000CE
:1000800087B1816087B98AB18FEF8AB9319BFECF82
:1000900088B1816088B980E890E827E033E03A953C
:1000A000F1F700008BB933E03A95F1F700001F99A2
:1000B00003C0392F30958323211105C08BB988B136
:1000C0008E7F88B9E3CF21509695892BE7CFF8949E
:0200D000FFCF60
:00000001FF

We'll verify that the property now holds:

$ machine-check-avr --system-hex-file (...)\calibration_fixed.hex --property "AG![EF![PORTD == 0]]"   
[2025-06-15T15:17:56Z INFO  machine_check] Starting verification.
[2025-06-15T15:17:56Z INFO  machine_check::verify] Verifying the inherent property first.
[2025-06-15T15:18:10Z INFO  machine_check::verify] The inherent property holds, proceeding to the given property.
[2025-06-15T15:18:10Z INFO  machine_check::verify] Verifying the given property.
[2025-06-15T15:18:10Z INFO  machine_check] Verification ended.
+----------------------------------+
|          Result: HOLDS           |
+----------------------------------+
|  Refinements:               513  |
|  Generated states:        14090  |
|  Final states:            13059  |
|  Generated transitions:   14603  |
|  Final transitions:       13573  |
+----------------------------------+

Perfect. Being happy with the result, we can uninstall the executable:

$ cargo uninstall machine-check-avr
    Removing (...)\.cargo\bin\machine-check-avr.exe

💡️ Generally, the major problem with verification of complex systems such as machine-code systems is that machine-check may not choose the refinements correctly and the time or memory needed for verification will be unacceptable. This will depend on the program and the cleverness of machine-check when using the chosen strategy, and can be investigated using the GUI.

🛠️ Currently, the ease of use of machine-check-avr leaves something to be desired: for example, if we want to verify a property depending on some line in the program source code, we have to obtain the mapping of the source-code line to the the value of the Program Counter (PC) ourselves. In the future, machine-check-avr could be extended to obtain this information automatically from a file with debug information.

⚠️ It is possible that due to the large amount of generated code, Rust will overflow its stack while optimising machine-check-avr, especially when compiling in debug mode. In that case, set the environment variable RUST_MIN_STACK=4194304. See also this Rust issue.