Skip to content
Merged
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
28 changes: 19 additions & 9 deletions .github/actions/setup-build-env/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ runs:
[ "$(uname -m)" != aarch64 ]; then
packages+=(gcc-arm-linux-gnueabihf)
fi
# An Arm64 host runs the AArch64 output natively, so mk/arm64.mk
# turns USE_QEMU off and the build resolves the interpreter and libc
# from the running system instead of from a cross sysroot.
if [ "$LINK_MODE" = dynamic ] && [ "$ARCHITECTURE" = arm64 ] && \
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
[ "$(uname -m)" != aarch64 ]; then
packages+=(gcc-aarch64-linux-gnu)
fi
sudo apt-get update -q -y
sudo apt-get install -q -y --no-install-recommends "${packages[@]}"

Expand Down Expand Up @@ -160,18 +167,21 @@ runs:
echo "$sha256 $file" | sha256sum -c -
sudo apt-get install -q -y --no-install-recommends "$file"

# An Arm64 runner is asked for so that the Arm output runs through the
# kernel rather than an emulator, and the step above is what lets
# mk/arm.mk see that. Read the decision back while it is fresh: a runner
# the probe stops matching would otherwise turn those jobs into slower
# copies of the x86 ones without saying so. An empty TARGET_EXEC is the
# Makefile saying nothing stands in front of the binary.
- name: Confirm the Arm output runs natively
if: runner.arch == 'ARM64' && inputs.architecture == 'arm'
# An Arm64 runner is asked for so that the Arm and AArch64 output runs
# through the kernel rather than an emulator: mk/arm.mk decides that from
# the step above, mk/arm64.mk from the host architecture alone. Read the
# decision back while it is fresh: a runner the probe stops matching would
# otherwise turn those jobs into slower copies of the x86 ones without
# saying so. An empty TARGET_EXEC is the Makefile saying nothing stands in
# front of the binary.
- name: Confirm the output runs natively
if: runner.arch == 'ARM64' && (inputs.architecture == 'arm' || inputs.architecture == 'arm64')
shell: bash
env:
ARCHITECTURE: ${{ inputs.architecture }}
run: |
set -euo pipefail
runner=$(make -np ARCH=arm 2>/dev/null | \
runner=$(make -np ARCH="$ARCHITECTURE" 2>/dev/null | \
sed -n 's/^TARGET_EXEC = *//p' | head -1)
if [ -n "$runner" ]; then
echo "Expected native execution, but the build selected: $runner"
Expand Down
24 changes: 14 additions & 10 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
fail-fast: false
matrix:
compiler: [gcc, clang]
architecture: [arm, riscv, x64]
architecture: [arm, arm64, riscv, x64]
link_mode: [static, dynamic]
env:
CC: ${{ matrix.compiler }}
Expand Down Expand Up @@ -80,7 +80,7 @@ jobs:
# clang-sanitized one, which puts a single cell of this matrix at
# roughly fifteen hours. Restoring the dimension means finding out
# why first.
architecture: [arm, riscv, x64]
architecture: [arm, arm64, riscv, x64]
link_mode: [static, dynamic]
env:
ARCH: ${{ matrix.architecture }}
Expand Down Expand Up @@ -110,7 +110,7 @@ jobs:
fail-fast: false
matrix:
compiler: [gcc, clang]
architecture: [arm, riscv, x64]
architecture: [arm, arm64, riscv, x64]
env:
CC: ${{ matrix.compiler }}
ARCH: ${{ matrix.architecture }}
Expand All @@ -130,36 +130,40 @@ jobs:
- name: Build stage 1 artifact
run: ./out/shecc --no-libc -o out/shecc-stage1.elf ./out/out.c

# An Arm64 runner executes the Arm output natively, so this covers the paths
# that the emulator would otherwise stand in for.
# An Arm64 runner executes both the Arm and the AArch64 output natively, so
# this covers the paths that the emulator would otherwise stand in for. For
# AArch64 that is the only place the real loader is exercised: QEMU-user maps
# the image itself, and on an x86-64 host it always presents 4 KiB pages.
host-arm:
name: arm/${{ matrix.link_mode }} on Arm64
name: ${{ matrix.architecture }}/${{ matrix.link_mode }} on Arm64
runs-on: ubuntu-24.04-arm
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
architecture: [arm, arm64]
link_mode: [static, dynamic]
env:
ARCH: ${{ matrix.architecture }}
DYNLINK: ${{ matrix.link_mode == 'dynamic' && '1' || '0' }}
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Set up the build environment
uses: ./.github/actions/setup-build-env
with:
architecture: arm
architecture: ${{ matrix.architecture }}
link-mode: ${{ matrix.link_mode }}
github-token: ${{ github.token }}
- name: Build artifacts
run: make ARCH=arm DYNLINK="$DYNLINK"
run: make ARCH="$ARCH" DYNLINK="$DYNLINK"
- name: Unit tests
run: make check ARCH=arm DYNLINK="$DYNLINK"
run: make check ARCH="$ARCH" DYNLINK="$DYNLINK"
- name: Upload the test logs
if: failure()
uses: actions/upload-artifact@v7
with:
name: logs-arm64-host-${{ matrix.link_mode }}
name: logs-arm64-host-${{ matrix.architecture }}-${{ matrix.link_mode }}
path: |
out/*.log
out/tests/*.log
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ USE_QEMU ?= 1
OUT ?= out
# Every architecture that can be selected as a build target. The first is the
# default when ARCH is not given.
ARCHS = arm riscv x64
ARCHS = arm arm64 riscv x64
ARCH ?= $(firstword $(ARCHS))
SRCDIR := $(shell find src -type d)
LIBDIR := $(shell find lib -type d)
Expand Down
44 changes: 33 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@

## Introduction

`shecc` is built from scratch, targeting 32-bit Arm, 32-bit RISC-V, and x86-64,
as a self-compiling compiler for a subset of the C language.
`shecc` is built from scratch, targeting 32-bit Arm, AArch64, 32-bit RISC-V,
and x86-64, as a self-compiling compiler for a subset of the C language.
Despite its simplistic nature, it is capable of performing basic optimization strategies as a standalone optimizing compiler.

### Features

* Generate executable Linux ELF binaries for ARMv7-A, RV32IM, and x86-64.
* Generate executable Linux ELF binaries for ARMv7-A, AArch64, RV32IM, and x86-64.
* Provide a minimal C standard library for basic I/O on GNU/Linux.
* The cross-compiler is written in ANSI C, making it compatible with most platforms.
* Include a self-contained C front-end with an integrated machine code generator; no external assembler or linker needed.
Expand All @@ -19,7 +19,7 @@ Despite its simplistic nature, it is capable of performing basic optimization st
* Develop a register allocation system that is compatible with RISC-style architectures.
* Implement an architecture-independent, [static single assignment](https://en.wikipedia.org/wiki/Static_single-assignment_form) (SSA)-based middle-end for enhanced optimizations.
* Support dynamic linking to allow generated executables to run with glibc.
* Emit both ELF32 (Arm, RISC-V) and ELF64 (x86-64) images; the ELF class follows the target pointer width.
* Emit both ELF32 (Arm, RISC-V) and ELF64 (AArch64, x86-64) images; the ELF class follows the target pointer width.

## Compatibility

Expand All @@ -41,6 +41,10 @@ syntax:
* function-like macros with parameters, `__VA_ARGS__`, stringification (`#`), and token pasting (`##`)

The Arm backend targets armv7hf with the Linux ABI, verified on Raspberry Pi 3.
The AArch64 backend follows AAPCS64 and supports static and eager-bound dynamic
linking, verified with QEMU AArch64 on eMag. Its images separate the load
segments by 64 KiB so they load under any of the 4 KiB, 16 KiB and 64 KiB
translation granules AArch64 Linux may be configured with.
The RISC-V backend targets RV32IM, verified with QEMU.
The x86-64 backend follows the System V AMD64 ABI and runs natively on an
x86-64 GNU/Linux host, so no emulator is involved.
Expand Down Expand Up @@ -86,7 +90,7 @@ the second stage bootstrapping would fail due to `qemu-arm` absence, and the

The dynamic linking mode needs an ELF interpreter and the matching glibc for the
target. The `x64` target resolves both from the host system, so it needs nothing
beyond an x86-64 GNU/Linux installation. The Arm and RISC-V targets need a
beyond an x86-64 GNU/Linux installation. The Arm, AArch64, and RISC-V targets need a
cross-compile GNU toolchain to obtain them.

For the Arm architecture, you can install the ARM GNU toolchain using `apt-get`:
Expand All @@ -98,21 +102,30 @@ Another approach is to manually download and install the toolchain from [ARM Dev
Select "x86_64 Linux hosted cross toolchains" - "AArch32 GNU/Linux target with hard float (arm-none-linux-gnueabihf)"
to download the toolchain.

For AArch64 dynamic binaries, install the matching toolchain and user emulator:

```shell
$ sudo apt-get install gcc-aarch64-linux-gnu qemu-user
```

Since `apt-get` does not provide the necessary RISC-V GNU toolchain, it must be downloaded manually if you want to
run a dynamically linked `shecc` targeting the RISC-V architecture. For instance, you can download and extract the
`riscv32-glibc-ubuntu-22.04-gcc.tar.xz` package from the [riscv-gnu-gcc](https://github.com/riscv-collab/riscv-gnu-toolchain) repository.

## Build and Verify

Configure which backend you want. `shecc` supports the ARMv7-A, RV32IM, and
x86-64 backends, with Arm as the default:
Configure which backend you want. `shecc` supports the ARMv7-A, AArch64,
RV32IM, and x86-64 backends, with Arm as the default:
```shell
$ make config ARCH=arm
# Target machine code switch to arm

$ make config ARCH=riscv
# Target machine code switch to riscv

$ make config ARCH=arm64
# Target machine code switch to arm64

$ make config ARCH=x64
# Target machine code switch to x64
```
Expand Down Expand Up @@ -145,7 +158,10 @@ $ make DYNLINK=1
SHECC out/shecc-stage2.elf

$ file out/shecc-stage2.elf
# ARCH=arm:
out/shecc-stage2.elf: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, not stripped
# ARCH=arm64:
out/shecc-stage2.elf: ELF 64-bit LSB executable, ARM aarch64, dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, no section header
```

For development builds with memory safety checks:
Expand All @@ -171,20 +187,26 @@ Compiler options:
Example 1: static linking mode
```shell
$ out/shecc -o fib tests/fib.c
$ chmod +x fib
$ qemu-arm fib
```

The compiler marks its own output executable, so neither example needs a
`chmod +x` in front of the run. Earlier revisions did: the mode used to be
whatever the libc that opened the file chose, which was 0666 for a
glibc-linked build.

An `x64` build produces a native binary, so `./fib` runs it directly with no
emulator in front.

Example 2: dynamic linking mode

Notice that `/usr/arm-linux-gnueabihf` is the ELF interpreter prefix. Since the path may be different if you manually install the ARM/RISC-V GNU toolchain instead of using `apt-get`, you should set the prefix to the actual path.
For AArch64, `/usr/aarch64-linux-gnu` is a typical ELF interpreter prefix.
The path may differ if you manually install a GNU toolchain, so set it to the
actual sysroot.
```shell
$ out/shecc --dynlink -o fib tests/fib.c
$ chmod +x fib

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think chmod +x fib should be preserved because the generated ELF file is not executable by default. Therefore, chmod is still necessary.

Otherwise, the following error may occur:

$ qemu-arm -L /usr/arm-linux-gnueabihf/ fib
Error while loading /home/drxiao/workspace/sysprog/shecc/fib: Exec format error

$ qemu-arm -L /usr/arm-linux-gnueabihf fib
$ qemu-arm -L /usr/arm-linux-gnueabihf fib # ARCH=arm
$ qemu-aarch64 -L /usr/aarch64-linux-gnu fib # ARCH=arm64
```

### Unit Tests
Expand Down
48 changes: 26 additions & 22 deletions lib/c.c
Original file line number Diff line number Diff line change
Expand Up @@ -241,13 +241,11 @@ void __str_base10(char *pb, int val)
int q, r, t;
int i = INT_BUF_LEN - 1;

/* On a 32-bit target, negating INT_MIN overflows and the digit loop below
* cannot make progress, so the value is spelled out directly. On LP64 the
* negation happens in a 64-bit register and the normal path is exact. This
* is an ordinary constant expression rather than a preprocessor conditional
* so that shecc can compile this file for either target.
/* val is an int on every target: negating INT_MIN overflows even when
* pointers and registers are 64-bit. Spell it directly so the digit loop
* never walks its stack buffer backwards indefinitely.
*/
if (__ptr_width == 4 && val == -2147483648) {
if (val == -2147483648) {
strncpy(pb + INT_BUF_LEN - 11, "-2147483648", 11);
return;
}
Expand Down Expand Up @@ -662,14 +660,14 @@ FILE *fopen(char *filename, char *mode)

if (!strcmp(mode, "wb"))
perm = 0x1fd;
#if defined(__riscv)
#if defined(__riscv) || defined(__aarch64__)
/* FIXME: mode not work currently in RISC-V */
fd = __syscall(__syscall_openat, -100, filename, 577, perm);
#else
fd = __syscall(__syscall_open, filename, 577, perm);
#endif
} else if (!strcmp(mode, "r") || !strcmp(mode, "rb")) {
#if defined(__riscv)
#if defined(__riscv) || defined(__aarch64__)
fd = __syscall(__syscall_openat, -100, filename, 0, 0);
#else
fd = __syscall(__syscall_open, filename, 0, 0);
Expand All @@ -692,6 +690,16 @@ int fclose(FILE *stream)
return 0;
}

int chmod(char *filename, int mode)
{
#if defined(__riscv) || defined(__aarch64__)
/* sys_fchmodat takes (dirfd, filename, mode); AT_FDCWD is -100. */
return __syscall(__syscall_fchmodat, -100, filename, mode);
#else
return __syscall(__syscall_chmod, filename, mode);
#endif
}

/* Read a byte from file descriptor. So the return value is either in the range
* of 0 to 127 for the character, or -1 on the end of file.
*/
Expand Down Expand Up @@ -738,32 +746,28 @@ int fputc(int c, FILE *stream)
int fseek(FILE *stream, int offset, int whence)
{
int result;
#if defined(__arm__)
result = __syscall(__syscall_lseek, stream, offset, whence);
#elif defined(__riscv)
/* No need to offset */

/* RV32 has only _llseek, which splits the offset and returns the result
* through a pointer. Every other target takes (fd, offset, whence)
* directly. lib/c.h rejects an architecture that is neither.
*/
#if defined(__riscv)
result = __syscall(__syscall_lseek, stream, 0, offset, NULL, whence);
#elif defined(__x86_64__)
/* x86-64 lseek(2) takes (fd, offset, whence) directly. */
result = __syscall(__syscall_lseek, stream, offset, whence);
#else
#error "Unsupported fseek support for current platform"
result = __syscall(__syscall_lseek, stream, offset, whence);
#endif
return result == -1;
}

int ftell(FILE *stream)
{
#if defined(__arm__)
return __syscall(__syscall_lseek, stream, 0, SEEK_CUR);
#elif defined(__riscv)
/* See fseek(): only RV32 needs the split-offset _llseek form. */
#if defined(__riscv)
int result;
__syscall(__syscall_lseek, stream, 0, 0, &result, SEEK_CUR);
return result;
#elif defined(__x86_64__)
return __syscall(__syscall_lseek, stream, 0, SEEK_CUR);
#else
#error "Unsupported ftell support for current platform"
return __syscall(__syscall_lseek, stream, 0, SEEK_CUR);
#endif
}

Expand Down
Loading