[sw/test] Add a test for the CRT.

This test can, for now, catch the regression fixed by the previous
commit. However, this test is not a fail-safe, since the linker can
arrange symbols in a way that causes the test to spuriously pass.

Signed-off-by: Miguel Young de la Sota <mcyoung@google.com>
diff --git a/ci/run_verilator_pytest.sh b/ci/run_verilator_pytest.sh
index ff57239..372da74 100755
--- a/ci/run_verilator_pytest.sh
+++ b/ci/run_verilator_pytest.sh
@@ -17,6 +17,7 @@
 TEST_TARGETS=(
   "examples/hello_usbdev/hello_usbdev_sim_verilator.elf"
   "tests/aes_test_sim_verilator.elf"
+  "tests/crt_test_sim_verilator.elf"
   "tests/dif_plic_sanitytest_sim_verilator.elf"
   "tests/dif_rv_timer_sanitytest_sim_verilator.elf"
   "tests/dif_uart_sanitytest_sim_verilator.elf"
diff --git a/sw/device/tests/crt_test.c b/sw/device/tests/crt_test.c
new file mode 100644
index 0000000..f588509
--- /dev/null
+++ b/sw/device/tests/crt_test.c
@@ -0,0 +1,113 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "sw/device/lib/arch/device.h"
+#include "sw/device/lib/base/log.h"
+#include "sw/device/lib/base/print.h"
+#include "sw/device/lib/base/stdasm.h"
+#include "sw/device/lib/runtime/check.h"
+#include "sw/device/lib/testing/test_status.h"
+#include "sw/device/lib/uart.h"
+
+// Symbols defined in sw/device/exts/common/flash_link.ld, which we use to
+// check that the CRT did what it was supposed to.
+extern char _bss_start;
+extern char _bss_end;
+extern char _data_start;
+extern char _data_end;
+extern char _data_init_start;
+
+// The addresses of the values above.
+static const uintptr_t bss_start_addr = (uintptr_t)&_bss_start;
+static const uintptr_t bss_end_addr = (uintptr_t)&_bss_end;
+static const uintptr_t data_start_addr = (uintptr_t)&_data_start;
+static const uintptr_t data_end_addr = (uintptr_t)&_data_end;
+static const uintptr_t data_init_start_addr = (uintptr_t)&_data_init_start;
+
+// Ensure that both .bss and .data are non-empty. The compiler will always keep
+// these symbols, since they're volatile.
+volatile char ensure_data_exists = 42;
+volatile char ensure_bss_exists;
+
+int main(int argc, char **argv) {
+  // NOTE: we cannot call any external functions until all checks of post-CRT
+  // state are complete; this is to ensure that our checks are not tainted by
+  // external functions.
+  //
+  // Among other things, this means we can't CHECK, since we can't initialize
+  // UART. Thus, any critical failures are handled by returning from main.
+  // To minimize the chance of things going wrong, we don't even bother placing
+  // the checks in their own function.
+
+  // Test core assumptions above the five addresses above. The test code
+  // must be able to assume these all hold.
+  //
+  // Note that performing these comparisons on their addresses is UB, and will
+  // cause this entire function to get deleted by the compiler.
+  if (&_bss_start > &_bss_end || &_data_start > &_data_end) {
+    // Something has gone terribly wrong and we have no hope of continuing the
+    // test, so we're going to return and let the test time out.
+    //
+    // The best method for debugging a failure like this is to stare at an
+    // instruction trace.
+    return 1;
+  }
+
+  // Ensure that .bss was *actually* zeroed at the start of execution. If it
+  // wasn't, we note the offset from _bss_start at which it wasn't.
+  char *bss = &_bss_start;
+  ptrdiff_t bss_len = &_bss_end - &_bss_start;
+  int bad_bss_index = -1;
+  for (int i = 0; i < bss_len; ++i) {
+    if (bss[i] != 0) {
+      bad_bss_index = i;
+      break;
+    }
+  }
+
+  // Similarly, ensure that .data has the values in the init section.
+  char *data = &_data_start;
+  char *data_init = &_data_init_start;
+  ptrdiff_t data_len = &_data_end - &_data_start;
+  int bad_data_index = -1;
+  for (int i = 0; i < data_len; ++i) {
+    if (data[i] != data_init[i]) {
+      bad_data_index = i;
+      break;
+    }
+  }
+
+  // End of post-CRT checks; begin actual assertions..
+  test_status_set(kTestStatusInTest);
+  // Initialize the UART to enable logging for non-DV simulation platforms.
+  if (kDeviceType != kDeviceSimDV) {
+    uart_init(kUartBaudrate);
+    base_set_stdout(uart_stdout);
+  }
+
+  CHECK(bss_start_addr % sizeof(uint32_t) == 0,
+        "_bss_start not word-aligned: 0x%08x", bss_start_addr);
+  CHECK(bss_end_addr % sizeof(uint32_t) == 0,
+        "_bss_end not word-aligned: 0x%08x", bss_end_addr);
+  CHECK(data_start_addr % sizeof(uint32_t) == 0,
+        "_data_start not word-aligned: 0x%08x", data_start_addr);
+  CHECK(data_end_addr % sizeof(uint32_t) == 0,
+        "_data_end not word-aligned: 0x%08x", data_end_addr);
+  CHECK(data_init_start_addr % sizeof(uint32_t) == 0,
+        "_data_init_start not word-aligned: 0x%08x", data_init_start_addr);
+
+  CHECK(bad_bss_index == -1, "found non-zero .bss byte at *0x%08x == 0x%02x",
+        bss_start_addr + bad_bss_index, (uint32_t)bss[bad_bss_index]);
+  CHECK(bad_data_index == -1,
+        "found bad .data byte at *0x%08x == 0x%02x, expected 0x%02x",
+        data_start_addr + bad_data_index, (uint32_t)data_init[bad_data_index]);
+
+  test_status_set(kTestStatusPassed);
+
+  // Unreachable code.
+  return 1;
+}
diff --git a/sw/device/tests/meson.build b/sw/device/tests/meson.build
index 512b613..e467d3c 100644
--- a/sw/device/tests/meson.build
+++ b/sw/device/tests/meson.build
@@ -87,3 +87,37 @@
     )
   endforeach
 endforeach
+
+foreach device_name, device_lib : sw_lib_arch_core_devices
+  crt_test_elf = executable(
+    'crt_test_' + device_name,
+    name_suffix: 'elf',
+    sources: ['crt_test.c'],
+    dependencies: [
+      riscv_crt,
+      device_lib,
+      sw_lib_irq_handlers,
+      sw_lib_testing_test_status,
+      # Explicitly do not pull in the test main; we need to run right after
+      # the CRT is done executing.
+      # sw_lib_testing_test_main,
+    ],
+  )
+
+  crt_test_embedded = custom_target(
+    'crt_test_' + device_name,
+    command: make_embedded_target,
+    input: crt_test_elf,
+    output: make_embedded_target_outputs,
+    build_by_default: true,
+  )
+
+  custom_target(
+    'crt_test_export_' + device_name,
+    command: export_embedded_target,
+    input: [crt_test_elf, crt_test_embedded],
+    output: 'crt_test_export_' + device_name,
+    build_always_stale: true,
+    build_by_default: true,
+  )
+endforeach