Skip to content

Terminate the simulation on exit() from firmware - #380

Merged
gatk555 merged 1 commit into
buserror:masterfrom
edgar-bonet:quit-on-exit
Sep 25, 2026
Merged

gatk555 merged 1 commit into
buserror:masterfrom
edgar-bonet:quit-on-exit

Conversation

@edgar-bonet

Copy link
Copy Markdown
Contributor

The firmware can end the simulation by sleeping with interrupts disabled. While this way of hanging the CPU makes a lot of sense, it is not the most intuitive.

The standard way to terminate a C program is by either calling exit() or returning from main(). It turns out the AVR C runtime supports stopping the firmware in both these standard ways. Returning from main() jumps to exit(), which disables interrupts and hangs the CPU in an infinite loop. The implementation is roughly like this:

; after the C runtime initializations:
.section .init9
    call main  ; run the firmware
    jmp exit   ; if main() returns, go to exit()

exit:
_exit:
    cli        ; disable interrupts
    rjmp .-2   ; hang the CPU

This pull request makes run_avr gracefully quit if the firmware executes the CPU-hanging instruction rjmp .-2 with interrupts disabled.

The actual condition used in the code is:

if (avr->state == cpu_Running && new_pc == avr->pc
    && avr->flash[new_pc] == 0xff && avr->flash[new_pc+1] == 0xcf
    && !avr->sreg[S_I])

Some of those tests may be redundant but, as I am not sure to understand all the subtleties of the simulation, I opted for defensive programming.

The feature has been tested with the following program:

#include <stdlib.h>
#include <avr/io.h>
#include <avr/sleep.h>
#include "avr_mcu_section.h"

AVR_MCU(F_CPU, "atmega328p");
AVR_MCU_SIMAVR_CONSOLE(&GPIOR0);

int main(void)
{
    const char *s = "Exiting.\r\n";
    for (const char *t = s; *t; t++)
        GPIOR0 = *t;
#if defined(QUIT_BY_SLEEP)
    sleep_mode();
#elif defined(QUIT_BY_EXIT)
    exit(0);
#endif
}

@edgar-bonet

Copy link
Copy Markdown
Contributor Author

I checked that, with this pull request, abort() is also a valid way of terminating the simulation. According to the avr-libc documentation, abort() is implemented as:

cli();
_exit(1);

@kittennbfive

Copy link
Copy Markdown
Contributor

+1 for this PR, i wondered why cli();while(1); does not stop the simulation.

@mofosyne

mofosyne commented Oct 6, 2025

Copy link
Copy Markdown

Oh cool does _exit(1); return 1 on simavr? Was hoping for a canonical way to use simavr for CI and having a way to return unit test results would be good.

@edgar-bonet

Copy link
Copy Markdown
Contributor Author

@mofosyne: No. The value passed to exit() (or _exit()) is not returned by simavr, but you could add the feature as an extension to this pull request. Quoting my own comment on PR #555:

Note that it should be possible to grab the value returned by main() (and passed to exit()) from r25:r24, and have run_avr return that. The above-mentioned pull request does not do that.

@edgar-bonet

Copy link
Copy Markdown
Contributor Author

@mofosyne: Here is a quick and dirty way to get simavr return the exit status of the firmware. Apply this pull request, then:

diff --git a/simavr/sim/run_avr.c b/simavr/sim/run_avr.c
index 863fc8e..0b4fd50 100644
--- a/simavr/sim/run_avr.c
+++ b/simavr/sim/run_avr.c
@@ -334,5 +334,7 @@ main(
 			break;
 	}
 
+	int exit_status = avr->data[24];  // r24 at program termination
 	avr_terminate(avr);
+	return exit_status;
 }

There is a caveat: if the firmware terminates by crashing, or by sleeping with interrupts off, then the returned value is meaningless. In such a case, it would be wiser for simavr to return 0, or maybe 1 on a crash. For this reason, I am not submitting this as a pull request, but you can use this solution if you are not concerned about this caveat.

@edgar-bonet

Copy link
Copy Markdown
Contributor Author

I just rebased, solved a couple of conflicts, and force-pushed.

@gatk555

gatk555 commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

The principle seems sound, but I do not like the repeated

avr->flash[avr->pc] == 0xff && avr->flash[avr->pc+1] == 0xcf

How about a macro?

#define JMP_TO_SELF(avr) (*(uint16_t *)(avr->flash + avr->pc) == 0xcfff) // Executing RJMP .-2

Very ugly, but explicit. Put it in sim_core.h for copying into other run loops.

When the firmware calls exit() or returns from main(), it ends up
running _exit(), which disables interrupts and hangs the CPU in an
infinite, empty loop. run_avr now terminates the simulation if the
CPU-hanging instruction (rjmp .-2) is executed with interrupts disabled.
@edgar-bonet

Copy link
Copy Markdown
Contributor Author

@gatk555: I like it! It does indeed make things clearer. I just amended the pull request (force-pushed).

@gatk555
gatk555 merged commit 95f248f into buserror:master Sep 25, 2026
8 checks passed
@edgar-bonet
edgar-bonet deleted the quit-on-exit branch September 25, 2026 17:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants