Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,18 @@ jobs:
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 20
sudo update-alternatives --install /usr/bin/cc cc /usr/bin/gcc 30
sudo update-alternatives --set cc /usr/bin/gcc
# Adding the i386 architecture above makes apt remove 206 amd64
# packages, pkgconf among them. Without pkg-config, Makefile.common
# does not find libelf, HAVE_LIBELF stays undefined, and the binary
# cannot load any firmware.
sudo apt-get install -y pkg-config
- name: CI-Build
run: |
# libelf-dev:i386 puts libelf.pc under /usr/lib/i386-linux-gnu,
# where pkg-config does not look by default. Without this,
# HAVE_LIBELF stays undefined and the 32 bit binary answers
# "ELF format is not supported by this build" for every firmware.
export PKG_CONFIG_PATH=/usr/lib/i386-linux-gnu/pkgconfig
make V=1 RELEASE=1
mkdir simavr_installed
make install RELEASE=1 DESTDIR=$(pwd)/simavr_installed/
Expand Down
664 changes: 664 additions & 0 deletions .github/workflows/release.yml

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,8 @@ perf.data
.bash_history
compile_commands.json
.cache

# Die Firmware für den Rauchtest in der CI gehört ins Repository, anders als
# jedes andere .elf hier. Ohne diese Ausnahme nimmt git add sie nicht mit --
# stillschweigend, was eine Weile kostet.
!ci/*.elf
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,15 @@ Extras:
graphically as “waveforms” with tools like _gtkwave_ (see below).
* You can even source your own VCD files (for example from your logic analyzer) and feed them into your simulation.
* There are a few examples of real life firmwares running on simavr, including OpenGL rendering of the display…
* There is support for _Arduino_, but no IDE integration
* There is some support for _Arduino_

## Integration:

- Simavr is the default debugging tool for supported AVR MCUs in [*PlatformIO*](https://platformio.org). If you select a debug build without specifying the tool, simavr will be started.
- There is basic support to use simavr inside the [*Arduino IDE 2*](https://docs.arduino.cc/software/ide-v2/tutorials/getting-started/ide-v2-downloading-and-installing/) as part of the debugging solution [PyAvrOCD](https://pyavrocd.io). Simply install a [debug-enabled Arduino board package](https://pyavrocd.io/supporting-packages/) and choose the programmer `simavr`. If you now start the debugger, the simulator will be started. There is some basic support for assembly-level debugging and the `PERIPHERAL` pane provides access to the I/O registers.
- The [*Arduino Maker Workshop*](https://github.com/thelastoutpostworkshop/arduino-maker-workshop) VS Code extension integrates simavr as well, again via PyAvrOCD. Due to using a recent version of Cortex-Debug, instruction-level debugging, memory inspection, and peripheral register display, are all supported.
- Simavr is one of many simulators integrated into [PICSimLab](https://github.com/lcgamboa/picsimlab), a realtime emulator of diverse development boards with MPLABX/avr-gdb debugger integration.
- [Simutron](https://sourceforge.net/projects/simutron/) is a GUI for simavr. However, the project does not appear to be active.

Documentation And Further Information
-------------------------------------
Expand Down
156 changes: 156 additions & 0 deletions check-floor.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
#!/bin/bash
#
# Copied from the avr-gdb repository, where it started; keep the two in step by
# hand. The one difference is --ucrt-ok below.
#
# Reports what a finished client demands of the system it will run on, and fails
# if it demands more than it was allowed to. One script for all three platforms,
# because the question is the same everywhere and only the place to read the
# answer differs:
#
# ELF the highest glibc version referenced readelf -V
# Mach-O the deployment target vtool -show-build
# PE the DLLs and functions it imports objdump -p
#
# Linux is the only one of the three with no way to declare that floor in the
# build; it falls out of the distribution one builds on. Hence this script: what
# cannot be stated has to be measured.
#
# Reading works across architectures -- readelf parses ELF directly instead of
# going through a target backend -- so an armhf client can be checked on an x86
# machine, which is what the arm job does.
#
# Usage:
# ./check-floor.sh <binary> <floor> [--armv6-hardfloat]
#
# ./check-floor.sh tool/avr-gdb 2.35
# ./check-floor.sh tool/avr-gdb 2.31 --armv6-hardfloat
# ./check-floor.sh tool/avr-gdb 11.0
# ./check-floor.sh tool/avr-gdb.exe 6.0
# ./check-floor.sh bin/simavr.exe 10.0 --ucrt-ok

set -eu

BIN=${1:?usage: check-floor.sh <binary> <floor> [--armv6-hardfloat] [--ucrt-ok]}
FLOOR=${2:?usage: check-floor.sh <binary> <floor> [--armv6-hardfloat] [--ucrt-ok]}
FLAGS=" ${*:3} "

has() { case "$FLAGS" in *" $1 "*) return 0 ;; *) return 1 ;; esac; }

fail() { echo "::error::$*" >&2; echo "FAILED: $*" >&2; exit 1; }

test -f "$BIN" || fail "no such file: $BIN"
echo "=== $BIN"
file "$BIN"
KIND=$(file -b "$BIN")

case "$KIND" in
*ELF*)
echo "--- libraries it needs at start ---"
readelf -d "$BIN" | grep NEEDED || echo "(none -- statically linked)"
readelf -l "$BIN" | grep -i interpreter || echo "(no interpreter)"

echo "--- glibc ---"
MAX=$(readelf -V "$BIN" | grep -o 'GLIBC_2\.[0-9]*' | sort -uV | tail -1 || true)
echo "highest glibc requirement: ${MAX:-none}"
[ -n "$MAX" ] || fail "no GLIBC_ references at all -- statically linked glibc? \
that is not portable: NSS and gconv load modules with dlopen at run time and \
then need the glibc it was linked against"
HAVE=${MAX#GLIBC_2.}
WANT=${FLOOR#2.}
[ "$HAVE" -le "$WANT" ] || fail "requires $MAX, allowed at most GLIBC_$FLOOR"

if has --armv6-hardfloat; then
echo "--- ARM ABI ---"
readelf -A "$BIN" | grep -E 'Tag_CPU_arch|Tag_FP_arch|Tag_ABI_VFP_args' || true
readelf -A "$BIN" | grep -q 'Tag_CPU_arch: v6' \
|| fail "not armv6 -- this drops the Pi 1 and Zero"
readelf -A "$BIN" | grep -q 'Tag_ABI_VFP_args: VFP registers' \
|| fail "not hard-float -- this does not run on Raspberry Pi OS"
fi
;;

*Mach-O*)
echo "--- libraries it needs at start ---"
otool -L "$BIN"
echo "--- outside the system? ---"
# Everything the binary names has to be a system library or relative to the
# binary itself. A Homebrew path means the archive only runs on a machine
# that has that formula installed at that prefix.
FOREIGN=$(otool -L "$BIN" | tail -n +2 | awk '{print $1}' \
| grep -vE '^(/usr/lib/|/System/|@rpath/|@executable_path/|@loader_path/)' || true)
if [ -n "$FOREIGN" ]; then
fail "references libraries outside the system: $(echo $FOREIGN) -- those would have to be bundled"
fi
echo "(nothing but system libraries and relative paths)"

echo "--- deployment target ---"
otool -l "$BIN" | grep -A4 LC_BUILD_VERSION || true
MINOS=$(vtool -show-build "$BIN" 2>/dev/null | awk '/minos/ {print $2; exit}' || true)
echo "minos: ${MINOS:-unknown}"
[ -n "$MINOS" ] || fail "could not read a deployment target from $BIN"
case "$MINOS" in
"$FLOOR"|"$FLOOR".*) ;;
*) fail "asked for macOS $FLOOR, binary says $MINOS -- is \
MACOSX_DEPLOYMENT_TARGET set for the whole build, not just for gdb?" ;;
esac
;;

*PE*|*MS\ Windows*)
OD=objdump
command -v x86_64-w64-mingw32-objdump >/dev/null && OD=x86_64-w64-mingw32-objdump

echo "--- subsystem version in the header (informational only) ---"
"$OD" -p "$BIN" | grep -iE 'Major(OSystem|Subsystem)Version|Minor(OSystem|Subsystem)Version' \
|| echo "(could not read the PE header with $OD)"
# This number is not the floor, however much it looks like one. It is a
# default the linker stamps, and the default differs per target: our two
# clients are built from the same sources with the same _WIN32_WINNT, and
# the 32-bit one says 4.0 while the 64-bit one says 5.2 -- which is simply
# the oldest Windows that ever had the respective architecture. The loader
# only refuses a binary whose number is HIGHER than the running Windows, so
# a low stamp can never make a program portable; it just fails to say
# anything. Comparing it against a floor would therefore pass every time.
#
# What does decide the floor is the import table: a function that Windows 7
# introduced makes the client refuse to start on Vista, no matter what any
# header field claims. So that is what we look at.

echo "--- DLLs it needs at start ---"
"$OD" -p "$BIN" | awk '/DLL Name:/ {print " " $3}' | sort -u

# Two DLL names are floors in themselves:
# api-ms-win-* the UCRT -- Windows 10, or a redistributable before it
# kernelbase.dll Windows 7
# mingw-w64 normally links the old msvcrt.dll, which every Windows has.
DLLS=$("$OD" -p "$BIN" | awk '/DLL Name:/ {print tolower($3)}' | sort -u)
case "$DLLS" in
*api-ms-win-crt*)
has --ucrt-ok || fail "imports the UCRT (api-ms-win-crt-*) -- that is a \
Windows 10 floor unless the user installs a redistributable"
echo "(imports the UCRT, as a MSYS2 UCRT64 build does: Windows 10)" ;;
esac
case "$DLLS" in
*kernelbase.dll*) fail "imports kernelbase.dll -- that DLL arrived with \
Windows 7, so the client cannot start on Vista" ;;
esac

# And a look at the other end: do we actually use anything newer than XP?
# Purely informational -- finding nothing would not prove the client runs on
# XP, only that these particular names are absent. But finding something
# confirms that _WIN32_WINNT=0x0600 is a real requirement and not a habit.
echo "--- functions that are Vista or newer (informational) ---"
"$OD" -p "$BIN" \
| grep -oE '\b(GetTickCount64|InitializeConditionVariable|SleepConditionVariableCS|InitializeSRWLock|AcquireSRWLockExclusive|CreateSymbolicLinkW|GetFinalPathNameByHandleW|InetNtopW|CancelIoEx|GetThreadId)\b' \
| sort -u | sed 's/^/ /' || echo " (none of the names we look for)"

echo "OK: nothing in the import table of $BIN demands more than Windows $FLOOR"
exit 0
;;

*)
fail "do not know how to read $BIN: $KIND"
;;
esac

echo "OK: $BIN stays within $FLOOR"
Binary file added ci/smoke-atmega328p.elf
Binary file not shown.
39 changes: 39 additions & 0 deletions ci/smoke.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* The firmware the release workflow runs simavr on, as a smoke test: it proves
* that the binary loads an ELF file (which is what libelf is for) and that the
* core executes instructions. --list-cores touches neither.
*
* It ends by itself. Returning from main leaves avr-libc's exit handler, which
* is `cli` followed by `rjmp .-2`, and simavr recognises that as "rjmp .-2 with
* interrupts off" and quits gracefully with status 0 (see gracefully_quit() in
* simavr/sim/sim_avr.c). So the test needs no timeout to come to an end, and the
* exit status is the whole assertion.
*
* Compiled with -g: a gdb session against simavr should be able to show a
* source line and read a variable, and without debug info it cannot.
*
* Deliberately without simavr's own headers: the firmware should not have to be
* rebuilt when those change.
*
* avr-gcc -mmcu=atmega328p -Og -g -o smoke-atmega328p.elf smoke.c
*/
#include <avr/io.h>
#include <stdint.h>

int main(void)
{
volatile uint16_t sum = 0;

/* a little arithmetic, a little SRAM, a little I/O -- enough that the
* core has to do something other than jump */
for (uint8_t i = 1; i <= 10; i++)
sum += i;

DDRB |= _BV(PB5);
if (sum == 55)
PORTB |= _BV(PB5);
else
PORTB &= (uint8_t)~_BV(PB5);

return 0;
}
60 changes: 60 additions & 0 deletions requirements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# What the release archives require

Every archive attached to a release is built on the oldest system we are willing
to support for that platform, and what it ends up demanding is measured after
the build rather than assumed. The numbers below come from `check-floor.sh`,
which the release workflow runs over every ELF object in the archive, not just
over the executable: a bundled library can demand more than the program does,
and the floor of an archive is the maximum over everything in it.

| Archive | Requires | Found on |
| ------- | -------- | -------- |
| `simavr-linux_x86_64.tar.gz` | GLIBC 2.34 | Ubuntu 22.04, Debian 12, RHEL 9 and later |
| `simavr-linux_i686.tar.gz` | GLIBC 2.34 | Ubuntu 22.04, Debian 12 and later |
| `simavr-linux_aarch64.tar.gz` | GLIBC 2.17 | RHEL 7, Raspberry Pi OS bullseye and later |
| `simavr-linux_armv6l.tar.gz` | GLIBC 2.28, ARMv6, hard float | Raspberry Pi OS buster and later, Pi 1 and Zero included |
| `simavr-linux_armv7l.tar.gz` | GLIBC 2.28, ARMv7, hard float | Debian 10, Raspberry Pi OS buster and later |
| `simavr-darwin_x86_64.tar.gz` | macOS 10.15 | Intel Macs |
| `simavr-darwin_arm64.tar.gz` | macOS 11.0 | Apple Silicon |
| `simavr-windows_amd64.tar.gz` | Windows 10 | 64 bit only |

GLIBC is backward compatible, so a newer system runs these binaries as well. The
arm archives ask for less than the system they were built on (Raspberry Pi OS
bullseye, GLIBC 2.31) because the code only references older symbols.

## Nothing else has to be installed

Each archive carries what it needs. On Linux that means `libsimavr`, and beside
it `libelf` and `libz`, found through an RPATH of `$ORIGIN`; only the C library
itself comes from the system. On macOS `libelf` and `libsimavr` are linked
statically and the binary names nothing but `/usr/lib/libSystem.B.dylib`. On
Windows the Universal CRT comes from the operating system, which is what makes
Windows 10 the floor, and any other DLL sits next to `simavr.exe`.

The graphical parts of simavr are not in these archives. `make install-simavr`
installs the simulator and its library, not `libsimavrparts`, so no archive
depends on OpenGL, GLUT or X11 and the simulator runs on a machine without a
display.

## What is not included

There is no DWARF support: `libdwarf` changes its API between releases, and the
version on the oldest system we build on is older than the sources expect. The
`info io_registers` command of simavr therefore does not work in these builds.
Everything a debugger needs is read by the debugger itself from the firmware's
ELF file, so a normal GDB session is unaffected.

The binaries are not stripped. They carry their debug information, which makes
the archives a few hundred kilobytes larger and a crash report useful.

## Checking it yourself

```
./check-floor.sh bin/simavr 2.34 # ELF: highest GLIBC version referenced
./check-floor.sh bin/simavr 11.0 # Mach-O: deployment target
./check-floor.sh bin/simavr.exe 10.0 --ucrt-ok # PE: the DLLs it imports
```

The script fails when a binary demands more than the version given, which is how
the release workflow notices the day a rebuilt archive would quietly raise its
own floor.