Skip to content
Open
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
26 changes: 19 additions & 7 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ jobs:
matrix:
compiler: [gcc, clang]
architecture: [arm, riscv]
link_mode: [static]
include:
- compiler: gcc
architecture: arm
link_mode: dynamic
- compiler: clang
architecture: arm
link_mode: dynamic
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand All @@ -18,25 +26,29 @@ jobs:
sudo apt-get install -q -y graphviz jq
sudo apt-get install -q -y qemu-user
sudo apt-get install -q -y build-essential
sudo apt-get install -q -y gcc-arm-linux-gnueabihf
- name: Build artifacts
env:
CC: ${{ matrix.compiler }}
run: |
make distclean config ARCH=${{ matrix.architecture }}
make ARCH=${{ matrix.architecture }} LINK_MODE=${{ matrix.link_mode }}
- name: IR regression tests
run: |
make check-snapshot || exit 1
make check-snapshot LINK_MODE=${{ matrix.link_mode }} || exit 1
- name: Sanitizer-enabled stage 0 tests
env:
CC: ${{ matrix.compiler }}
run: |
make check-sanitizer || exit 1
make check-sanitizer LINK_MODE=${{ matrix.link_mode }} || exit 1
- name: Unit tests
run: |
make check || exit 1
make check LINK_MODE=${{ matrix.link_mode }} || exit 1

host-arm:
runs-on: ubuntu-24.04
strategy:
matrix:
link_mode: [static, dynamic]
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand All @@ -52,9 +64,9 @@ jobs:
apt-get update -qq -y
apt-get install -yqq build-essential
run: |
make config ARCH=arm
make check-sanitizer || exit 1
make check || exit 1
make ARCH=arm LINK_MODE=${{ matrix.link_mode }}
make check-sanitizer LINK_MODE=${{ matrix.link_mode }} || exit 1
make check LINK_MODE=${{ matrix.link_mode }} || exit 1

coding-style:
runs-on: ubuntu-24.04
Expand Down
60 changes: 39 additions & 21 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,28 @@ ARCH ?= $(firstword $(ARCHS))
HOST_ARCH = $(shell arch 2>/dev/null)
SRCDIR := $(shell find src -type d)
LIBDIR := $(shell find lib -type d)
LINK_MODES = static dynamic

BUILTIN_LIBC_SOURCE ?= c.c
BUILTIN_LIBC_HEADER := c.h
STAGE0_FLAGS ?= --dump-ir
STAGE1_FLAGS ?=
ifeq ($(LINK_MODE),dynamic)
ifeq ($(ARCH),riscv)
# TODO: implement dynamic linking for RISC-V.
$(error "Dynamic linking mode is not implemented for RISC-V")
endif
STAGE0_FLAGS += --dynlink
STAGE1_FLAGS += --dynlink
endif

SRCS := $(wildcard $(patsubst %,%/main.c, $(SRCDIR)))
OBJS := $(SRCS:%.c=$(OUT)/%.o)
deps := $(OBJS:%.o=%.o.d)
TESTS := $(wildcard tests/*.c)
TESTBINS := $(TESTS:%.c=$(OUT)/%.elf)
SNAPSHOTS := $(foreach SNAPSHOT_ARCH,$(ARCHS), $(patsubst tests/%.c, tests/snapshots/%-$(SNAPSHOT_ARCH).json, $(TESTS)))
SNAPSHOTS = $(foreach SNAPSHOT_ARCH,$(ARCHS), $(patsubst tests/%.c, tests/snapshots/%-$(SNAPSHOT_ARCH)-static.json, $(TESTS)))
SNAPSHOTS += $(patsubst tests/%.c, tests/snapshots/%-arm-dynamic.json, $(TESTS))

all: config bootstrap

Expand All @@ -72,44 +87,46 @@ config:

$(OUT)/tests/%.elf: tests/%.c $(OUT)/$(STAGE0)
$(VECHO) " SHECC\t$@\n"
$(Q)$(OUT)/$(STAGE0) --dump-ir -o $@ $< > $(basename $@).log ; \
$(Q)$(OUT)/$(STAGE0) $(STAGE0_FLAGS) -o $@ $< > $(basename $@).log ; \
chmod +x $@ ; $(PRINTF) "Running $@ ...\n"
$(Q)$(TARGET_EXEC) $@ && $(call pass)

check: check-stage0 check-stage2

check-stage0: $(OUT)/$(STAGE0) $(TESTBINS) tests/driver.sh
$(VECHO) " TEST STAGE 0\n"
tests/driver.sh 0
tests/driver.sh 0 $(LINK_MODE)

check-stage2: $(OUT)/$(STAGE2) $(TESTBINS) tests/driver.sh
$(VECHO) " TEST STAGE 2\n"
tests/driver.sh 2
tests/driver.sh 2 $(LINK_MODE)

check-sanitizer: $(OUT)/$(STAGE0)-sanitizer tests/driver.sh
$(VECHO) " TEST STAGE 0 (with sanitizers)\n"
$(Q)cp $(OUT)/$(STAGE0)-sanitizer $(OUT)/shecc
tests/driver.sh 0
tests/driver.sh 0 $(LINK_MODE)
$(Q)rm $(OUT)/shecc

check-snapshots: $(OUT)/$(STAGE0) $(SNAPSHOTS) tests/check-snapshots.sh
$(Q)$(foreach SNAPSHOT_ARCH, $(ARCHS), $(MAKE) distclean config check-snapshot ARCH=$(SNAPSHOT_ARCH) --silent;)
$(VECHO) "Switching backend back to %s\n" $(ARCH)
$(Q)$(MAKE) distclean config ARCH=$(ARCH) --silent
$(Q)$(foreach SNAPSHOT_ARCH, $(ARCHS), $(MAKE) distclean config check-snapshot ARCH=$(SNAPSHOT_ARCH) LINK_MODE=static --silent;)
$(Q)$(MAKE) distclean config check-snapshot ARCH=arm LINK_MODE=dynamic --silent
$(VECHO) "Switching backend back to %s (%s mode)\n" arm static
$(Q)$(MAKE) distclean config ARCH=arm --silent

check-snapshot: $(OUT)/$(STAGE0) tests/check-snapshots.sh
$(VECHO) "Checking snapshot for %s\n" $(ARCH)
tests/check-snapshots.sh $(ARCH)
$(VECHO) "Checking snapshot for %s (%s mode)\n" $(ARCH) $(LINK_MODE)
tests/check-snapshots.sh $(ARCH) $(LINK_MODE)
$(VECHO) " OK\n"

update-snapshots: tests/update-snapshots.sh
$(Q)$(foreach SNAPSHOT_ARCH, $(ARCHS), $(MAKE) distclean config update-snapshot ARCH=$(SNAPSHOT_ARCH) --silent;)
$(VECHO) "Switching backend back to %s\n" $(ARCH)
$(Q)$(MAKE) distclean config ARCH=$(ARCH) --silent
$(Q)$(foreach SNAPSHOT_ARCH, $(ARCHS), $(MAKE) distclean config update-snapshot ARCH=$(SNAPSHOT_ARCH) LINK_MODE=static --silent;)
$(Q)$(MAKE) distclean config update-snapshot ARCH=arm LINK_MODE=dynamic --silent
$(VECHO) "Switching backend back to %s (%s mode)\n" arm static
$(Q)$(MAKE) distclean config ARCH=arm --silent

update-snapshot: $(OUT)/$(STAGE0) tests/update-snapshots.sh
$(VECHO) "Updating snapshot for %s\n" $(ARCH)
tests/update-snapshots.sh $(ARCH)
$(VECHO) "Updating snapshot for %s (%s mode)\n" $(ARCH) $(LINK_MODE)
tests/update-snapshots.sh $(ARCH) $(LINK_MODE)
$(VECHO) " OK\n"

$(OUT)/%.o: %.c
Expand All @@ -122,11 +139,12 @@ $(OUT)/norm-lf: tools/norm-lf.c
$(VECHO) " CC+LD\t$@\n"
$(Q)$(CC) $(CFLAGS) -o $@ $^

$(OUT)/libc.inc: $(OUT)/inliner $(OUT)/norm-lf $(LIBDIR)/c.c
$(OUT)/libc.inc: $(OUT)/inliner $(OUT)/norm-lf $(LIBDIR)/$(BUILTIN_LIBC_SOURCE) $(LIBDIR)/$(BUILTIN_LIBC_HEADER)
$(VECHO) " GEN\t$@\n"
$(Q)$(OUT)/norm-lf $(LIBDIR)/c.c $(OUT)/c.normalized.c
$(Q)$(OUT)/inliner $(OUT)/c.normalized.c $@
$(Q)$(RM) $(OUT)/c.normalized.c
$(Q)$(OUT)/norm-lf $(LIBDIR)/$(BUILTIN_LIBC_SOURCE) $(OUT)/c.normalized.c
$(Q)$(OUT)/norm-lf $(LIBDIR)/$(BUILTIN_LIBC_HEADER) $(OUT)/c.normalized.h
$(Q)$(OUT)/inliner $(OUT)/c.normalized.c $(OUT)/c.normalized.h $@
$(Q)$(RM) $(OUT)/c.normalized.c $(OUT)/c.normalized.h

$(OUT)/inliner: tools/inliner.c
$(VECHO) " CC+LD\t$@\n"
Expand All @@ -143,12 +161,12 @@ $(OUT)/$(STAGE0)-sanitizer: $(OUT)/libc.inc $(OBJS)
$(OUT)/$(STAGE1): $(OUT)/$(STAGE0)
$(Q)$(STAGE1_CHECK_CMD)
$(VECHO) " SHECC\t$@\n"
$(Q)$(OUT)/$(STAGE0) --dump-ir -o $@ $(SRCDIR)/main.c > $(OUT)/shecc-stage1.log
$(Q)$(OUT)/$(STAGE0) $(STAGE0_FLAGS) -o $@ $(SRCDIR)/main.c > $(OUT)/shecc-stage1.log
$(Q)chmod a+x $@

$(OUT)/$(STAGE2): $(OUT)/$(STAGE1)
$(VECHO) " SHECC\t$@\n"
$(Q)$(TARGET_EXEC) $(OUT)/$(STAGE1) -o $@ $(SRCDIR)/main.c
$(Q)$(TARGET_EXEC) $(OUT)/$(STAGE1) $(STAGE1_FLAGS) -o $@ $(SRCDIR)/main.c

bootstrap: $(OUT)/$(STAGE2)
$(Q)chmod 775 $(OUT)/$(STAGE2)
Expand Down
42 changes: 39 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,19 @@ To execute the snapshot test, install the packages below:
$ sudo apt-get install graphviz jq
```

Additionally, because `shecc` supports the dynamic linking mode for the Arm architecture,
it needs to install the ARM GNU toolchain to obtain the ELF interpreter and other dependencies:
```shell
$ sudo apt-get install gcc-arm-linux-gnueabihf
```
Another approach is to manually download and install the toolchain from [ARM Developer website](https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads).

Select "x86_64 Linux hosted cross toolchains" - "AArch32 GNU/Linux target with hard float (arm-none-linux-gnueabihf)" to download the toolchain.

## Build and Verify

Configure which backend you want, `shecc` supports ARMv7-A and RV32IM backend:
```
```shell
$ make config ARCH=arm
# Target machine code switch to Arm

Expand All @@ -86,13 +95,29 @@ $ make config ARCH=riscv
```

Run `make` and you should see this:
```shell
$ make
CC+LD out/inliner
GEN out/libc.inc
CC out/src/main.o
LD out/shecc
SHECC out/shecc-stage1.elf
SHECC out/shecc-stage2.elf
```

Run `make LINK_MODE=dynamic` to use the dynamic linking mode and generate the dynamically linked compiler:
```shell
# If using the dynamic linking mode, you should add 'LINK_MODE=dynamic' for each 'make' command.
$ make LINK_MODE=dynamic
CC+LD out/inliner
GEN out/libc.inc
CC out/src/main.o
LD out/shecc
SHECC out/shecc-stage1.elf
SHECC out/shecc-stage2.elf

$ file out/shecc-stage2.elf
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
```

For development builds with memory safety checks:
Expand All @@ -103,22 +128,32 @@ $ make check-sanitizer

File `out/shecc` is the first stage compiler. Its usage:
```shell
$ shecc [-o output] [+m] [--no-libc] [--dump-ir] <infile.c>
$ shecc [-o output] [+m] [--no-libc] [--dump-ir] [--dynlink] <infile.c>
```

Compiler options:
- `-o` : Specify output file name (default: `out.elf`)
- `+m` : Use hardware multiplication/division instructions (default: disabled)
- `--no-libc` : Exclude embedded C library (default: embedded)
- `--dump-ir` : Dump intermediate representation (IR)
- `--dynlink` : Use dynamic linking (default: disabled)

Example:
Example 1: static linking mode
```shell
$ out/shecc -o fib tests/fib.c
$ chmod +x fib
$ qemu-arm fib
```

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 GNU toolchain instead of using `apt-get`, you should set the prefix to the actual path.
```shell
$ out/shecc --dynlink -o fib tests/fib.c
$ chmod +x fib
$ qemu-arm -L /usr/arm-linux-gnueabihf fib
```

### IR Regression Tests

To ensure the consistency of frontend (lexer, parser) behavior when working on it, the snapshot test is introduced.
Expand All @@ -142,6 +177,7 @@ use `update-snapshot` / `check-snapshot` instead.

`shecc` comes with a comprehensive test suite (200+ test cases). To run the tests:
```shell
# Add 'LINK_MODE=dynamic' if using the dynamic linking mode.
$ make check # Run all tests (stage 0 and stage 2)
$ make check-stage0 # Test stage 0 compiler only
$ make check-stage2 # Test stage 2 compiler only
Expand Down
50 changes: 1 addition & 49 deletions lib/c.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,50 +6,9 @@
*/

/* minimal libc implementation */

#define NULL 0

#define bool _Bool
#define true 1
#define false 0

#define INT_MAX 0x7fffffff
#define INT_MIN 0x80000000

#if defined(__arm__)
#define __SIZEOF_POINTER__ 4
#define __syscall_exit 1
#define __syscall_read 3
#define __syscall_write 4
#define __syscall_close 6
#define __syscall_open 5
#define __syscall_mmap2 192
#define __syscall_munmap 91

#elif defined(__riscv)
#define __SIZEOF_POINTER__ 4
#define __syscall_exit 93
#define __syscall_read 63
#define __syscall_write 64
#define __syscall_close 57
#define __syscall_open 1024
#define __syscall_openat 56
#define __syscall_mmap2 222
#define __syscall_munmap 215

#else /* Only Arm32 and RV32 are supported */
#error "Unsupported architecture"
#endif

#include "c.h"
#define INT_BUF_LEN 16

typedef int FILE;

/* va_list support for variadic functions */
typedef int *va_list;

void abort(void);

int strlen(char *str)
{
/* process the string by checking 4 characters (a 32-bit word) at a time */
Expand Down Expand Up @@ -584,18 +543,11 @@ int fputc(int c, FILE *stream)
return c;
}

/* Non-portable: Assume page size is 4KiB */
#define PAGESIZE 4096

#define CHUNK_SIZE_FREED_MASK 1
#define CHUNK_SIZE_SZ_MASK 0xFFFFFFFE
#define CHUNK_GET_SIZE(size) (size & CHUNK_SIZE_SZ_MASK)
#define IS_CHUNK_GET_FREED(size) (size & CHUNK_SIZE_FREED_MASK)

/* Minimum alignment for all memory allocations. */
#define MIN_ALIGNMENT 8
#define ALIGN_UP(val, align) (((val) + (align) - 1) & ~((align) - 1))

typedef struct chunk {
struct chunk *next, *prev;
int size;
Expand Down
Loading