Skip to content

Commit 3cbbdee

Browse files
authored
Merge pull request #244 from DrXiao/feat/support-dynamic-linking
Support Arm dynamic linking
2 parents 60dccd5 + 7a159bb commit 3cbbdee

34 files changed

+2588
-325
lines changed

.github/workflows/main.yml

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ jobs:
99
matrix:
1010
compiler: [gcc, clang]
1111
architecture: [arm, riscv]
12+
link_mode: [static]
13+
include:
14+
- compiler: gcc
15+
architecture: arm
16+
link_mode: dynamic
17+
- compiler: clang
18+
architecture: arm
19+
link_mode: dynamic
1220
steps:
1321
- name: Checkout code
1422
uses: actions/checkout@v4
@@ -18,28 +26,52 @@ jobs:
1826
sudo apt-get install -q -y graphviz jq
1927
sudo apt-get install -q -y qemu-user
2028
sudo apt-get install -q -y build-essential
29+
sudo apt-get install -q -y gcc-arm-linux-gnueabihf
30+
- name: Determine static or dynamic linking mode
31+
id: determine-mode
32+
run: |
33+
if [ "${{ matrix.link_mode }}" = "dynamic" ]; then
34+
echo "Use dynamic linking mode"
35+
echo "DYNLINK=1" >> "$GITHUB_OUTPUT"
36+
else
37+
echo "Use static linking mode"
38+
echo "DYNLINK=0" >> "$GITHUB_OUTPUT"
39+
fi
2140
- name: Build artifacts
2241
env:
2342
CC: ${{ matrix.compiler }}
2443
run: |
25-
make distclean config ARCH=${{ matrix.architecture }}
44+
make ARCH=${{ matrix.architecture }} DYNLINK=${{ steps.determine-mode.outputs.DYNLINK }}
2645
- name: IR regression tests
2746
run: |
28-
make check-snapshot || exit 1
47+
make check-snapshot DYNLINK=${{ steps.determine-mode.outputs.DYNLINK }} || exit 1
2948
- name: Sanitizer-enabled stage 0 tests
3049
env:
3150
CC: ${{ matrix.compiler }}
3251
run: |
33-
make check-sanitizer || exit 1
52+
make check-sanitizer DYNLINK=${{ steps.determine-mode.outputs.DYNLINK }} || exit 1
3453
- name: Unit tests
3554
run: |
36-
make check || exit 1
55+
make check DYNLINK=${{ steps.determine-mode.outputs.DYNLINK }} || exit 1
3756
3857
host-arm:
3958
runs-on: ubuntu-24.04
59+
strategy:
60+
matrix:
61+
link_mode: [static, dynamic]
4062
steps:
4163
- name: Checkout code
4264
uses: actions/checkout@v4
65+
- name: Determine static or dynamic linking mode
66+
id: determine-mode
67+
run: |
68+
if [ "${{ matrix.link_mode }}" = "dynamic" ]; then
69+
echo "Use dynamic linking mode"
70+
echo "DYNLINK=1" >> "$GITHUB_OUTPUT"
71+
else
72+
echo "Use static linking mode"
73+
echo "DYNLINK=0" >> "$GITHUB_OUTPUT"
74+
fi
4375
- name: Build artifacts
4476
# The GitHub Action for non-x86 CPU
4577
# https://github.com/uraimo/run-on-arch-action
@@ -52,9 +84,9 @@ jobs:
5284
apt-get update -qq -y
5385
apt-get install -yqq build-essential
5486
run: |
55-
make config ARCH=arm
56-
make check-sanitizer || exit 1
57-
make check || exit 1
87+
make ARCH=arm DYNLINK=${{ steps.determine-mode.outputs.DYNLINK }}
88+
make check-sanitizer DYNLINK=${{ steps.determine-mode.outputs.DYNLINK }} || exit 1
89+
make check DYNLINK=${{ steps.determine-mode.outputs.DYNLINK }} || exit 1
5890
5991
coding-style:
6092
runs-on: ubuntu-24.04

Makefile

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,27 @@ HOST_ARCH = $(shell arch 2>/dev/null)
4343
SRCDIR := $(shell find src -type d)
4444
LIBDIR := $(shell find lib -type d)
4545

46+
BUILTIN_LIBC_SOURCE ?= c.c
47+
BUILTIN_LIBC_HEADER := c.h
48+
STAGE0_FLAGS ?= --dump-ir
49+
STAGE1_FLAGS ?=
50+
DYNLINK ?= 0
51+
ifeq ($(DYNLINK),1)
52+
ifeq ($(ARCH),riscv)
53+
# TODO: implement dynamic linking for RISC-V.
54+
$(error "Dynamic linking mode is not implemented for RISC-V")
55+
endif
56+
STAGE0_FLAGS += --dynlink
57+
STAGE1_FLAGS += --dynlink
58+
endif
59+
4660
SRCS := $(wildcard $(patsubst %,%/main.c, $(SRCDIR)))
4761
OBJS := $(SRCS:%.c=$(OUT)/%.o)
4862
deps := $(OBJS:%.o=%.o.d)
4963
TESTS := $(wildcard tests/*.c)
5064
TESTBINS := $(TESTS:%.c=$(OUT)/%.elf)
51-
SNAPSHOTS := $(foreach SNAPSHOT_ARCH,$(ARCHS), $(patsubst tests/%.c, tests/snapshots/%-$(SNAPSHOT_ARCH).json, $(TESTS)))
65+
SNAPSHOTS = $(foreach SNAPSHOT_ARCH,$(ARCHS), $(patsubst tests/%.c, tests/snapshots/%-$(SNAPSHOT_ARCH)-static.json, $(TESTS)))
66+
SNAPSHOTS += $(patsubst tests/%.c, tests/snapshots/%-arm-dynamic.json, $(TESTS))
5267

5368
all: config bootstrap
5469

@@ -72,44 +87,61 @@ config:
7287

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

79-
check: check-stage0 check-stage2
94+
check: check-stage0 check-stage2 check-abi-stage0 check-abi-stage2
8095

8196
check-stage0: $(OUT)/$(STAGE0) $(TESTBINS) tests/driver.sh
8297
$(VECHO) " TEST STAGE 0\n"
83-
tests/driver.sh 0
98+
tests/driver.sh 0 $(DYNLINK)
8499

85100
check-stage2: $(OUT)/$(STAGE2) $(TESTBINS) tests/driver.sh
86101
$(VECHO) " TEST STAGE 2\n"
87-
tests/driver.sh 2
102+
tests/driver.sh 2 $(DYNLINK)
88103

89104
check-sanitizer: $(OUT)/$(STAGE0)-sanitizer tests/driver.sh
90105
$(VECHO) " TEST STAGE 0 (with sanitizers)\n"
91106
$(Q)cp $(OUT)/$(STAGE0)-sanitizer $(OUT)/shecc
92-
tests/driver.sh 0
107+
tests/driver.sh 0 $(DYNLINK)
93108
$(Q)rm $(OUT)/shecc
94109

95110
check-snapshots: $(OUT)/$(STAGE0) $(SNAPSHOTS) tests/check-snapshots.sh
96-
$(Q)$(foreach SNAPSHOT_ARCH, $(ARCHS), $(MAKE) distclean config check-snapshot ARCH=$(SNAPSHOT_ARCH) --silent;)
97-
$(VECHO) "Switching backend back to %s\n" $(ARCH)
98-
$(Q)$(MAKE) distclean config ARCH=$(ARCH) --silent
111+
$(Q)$(foreach SNAPSHOT_ARCH, $(ARCHS), $(MAKE) distclean config check-snapshot ARCH=$(SNAPSHOT_ARCH) DYNLINK=0 --silent;)
112+
$(Q)$(MAKE) distclean config check-snapshot ARCH=arm DYNLINK=1 --silent
113+
$(VECHO) "Switching backend back to %s (DYNLINK=0)\n" arm
114+
$(Q)$(MAKE) distclean config ARCH=arm DYNLINK=0 --silent
99115

100116
check-snapshot: $(OUT)/$(STAGE0) tests/check-snapshots.sh
101-
$(VECHO) "Checking snapshot for %s\n" $(ARCH)
102-
tests/check-snapshots.sh $(ARCH)
117+
$(VECHO) "Checking snapshot for %s (DYNLINK=%s)\n" $(ARCH) $(DYNLINK)
118+
tests/check-snapshots.sh $(ARCH) $(DYNLINK)
103119
$(VECHO) " OK\n"
104120

121+
# TODO: Add an ABI conformance test suite for the RISC-V architecture
122+
check-abi-stage0: $(OUT)/$(STAGE0)
123+
$(Q)if [ "$(ARCH)" = "arm" ]; then \
124+
tests/$(ARCH)-abi.sh 0 $(DYNLINK); \
125+
else \
126+
echo "Skip ABI compliance validation"; \
127+
fi
128+
129+
check-abi-stage2: $(OUT)/$(STAGE2)
130+
$(Q)if [ "$(ARCH)" = "arm" ]; then \
131+
tests/$(ARCH)-abi.sh 2 $(DYNLINK); \
132+
else \
133+
echo "Skip ABI compliance validation"; \
134+
fi
135+
105136
update-snapshots: tests/update-snapshots.sh
106-
$(Q)$(foreach SNAPSHOT_ARCH, $(ARCHS), $(MAKE) distclean config update-snapshot ARCH=$(SNAPSHOT_ARCH) --silent;)
107-
$(VECHO) "Switching backend back to %s\n" $(ARCH)
108-
$(Q)$(MAKE) distclean config ARCH=$(ARCH) --silent
137+
$(Q)$(foreach SNAPSHOT_ARCH, $(ARCHS), $(MAKE) distclean config update-snapshot ARCH=$(SNAPSHOT_ARCH) DYNLINK=0 --silent;)
138+
$(Q)$(MAKE) distclean config update-snapshot ARCH=arm DYNLINK=1 --silent
139+
$(VECHO) "Switching backend back to %s (DYNLINK=0)\n" arm
140+
$(Q)$(MAKE) distclean config ARCH=arm DYNLINK=0 --silent
109141

110142
update-snapshot: $(OUT)/$(STAGE0) tests/update-snapshots.sh
111-
$(VECHO) "Updating snapshot for %s\n" $(ARCH)
112-
tests/update-snapshots.sh $(ARCH)
143+
$(VECHO) "Updating snapshot for %s (DYNLINK=%s)\n" $(ARCH) $(DYNLINK)
144+
tests/update-snapshots.sh $(ARCH) $(DYNLINK)
113145
$(VECHO) " OK\n"
114146

115147
$(OUT)/%.o: %.c
@@ -122,11 +154,12 @@ $(OUT)/norm-lf: tools/norm-lf.c
122154
$(VECHO) " CC+LD\t$@\n"
123155
$(Q)$(CC) $(CFLAGS) -o $@ $^
124156

125-
$(OUT)/libc.inc: $(OUT)/inliner $(OUT)/norm-lf $(LIBDIR)/c.c
157+
$(OUT)/libc.inc: $(OUT)/inliner $(OUT)/norm-lf $(LIBDIR)/$(BUILTIN_LIBC_SOURCE) $(LIBDIR)/$(BUILTIN_LIBC_HEADER)
126158
$(VECHO) " GEN\t$@\n"
127-
$(Q)$(OUT)/norm-lf $(LIBDIR)/c.c $(OUT)/c.normalized.c
128-
$(Q)$(OUT)/inliner $(OUT)/c.normalized.c $@
129-
$(Q)$(RM) $(OUT)/c.normalized.c
159+
$(Q)$(OUT)/norm-lf $(LIBDIR)/$(BUILTIN_LIBC_SOURCE) $(OUT)/c.normalized.c
160+
$(Q)$(OUT)/norm-lf $(LIBDIR)/$(BUILTIN_LIBC_HEADER) $(OUT)/c.normalized.h
161+
$(Q)$(OUT)/inliner $(OUT)/c.normalized.c $(OUT)/c.normalized.h $@
162+
$(Q)$(RM) $(OUT)/c.normalized.c $(OUT)/c.normalized.h
130163

131164
$(OUT)/inliner: tools/inliner.c
132165
$(VECHO) " CC+LD\t$@\n"
@@ -143,12 +176,12 @@ $(OUT)/$(STAGE0)-sanitizer: $(OUT)/libc.inc $(OBJS)
143176
$(OUT)/$(STAGE1): $(OUT)/$(STAGE0)
144177
$(Q)$(STAGE1_CHECK_CMD)
145178
$(VECHO) " SHECC\t$@\n"
146-
$(Q)$(OUT)/$(STAGE0) --dump-ir -o $@ $(SRCDIR)/main.c > $(OUT)/shecc-stage1.log
179+
$(Q)$(OUT)/$(STAGE0) $(STAGE0_FLAGS) -o $@ $(SRCDIR)/main.c > $(OUT)/shecc-stage1.log
147180
$(Q)chmod a+x $@
148181

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

153186
bootstrap: $(OUT)/$(STAGE2)
154187
$(Q)chmod 775 $(OUT)/$(STAGE2)

README.md

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,19 @@ To execute the snapshot test, install the packages below:
7474
$ sudo apt-get install graphviz jq
7575
```
7676

77+
Additionally, because `shecc` supports the dynamic linking mode for the Arm architecture,
78+
it needs to install the ARM GNU toolchain to obtain the ELF interpreter and other dependencies:
79+
```shell
80+
$ sudo apt-get install gcc-arm-linux-gnueabihf
81+
```
82+
Another approach is to manually download and install the toolchain from [ARM Developer website](https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads).
83+
84+
Select "x86_64 Linux hosted cross toolchains" - "AArch32 GNU/Linux target with hard float (arm-none-linux-gnueabihf)" to download the toolchain.
85+
7786
## Build and Verify
7887

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

@@ -86,13 +95,29 @@ $ make config ARCH=riscv
8695
```
8796

8897
Run `make` and you should see this:
98+
```shell
99+
$ make
100+
CC+LD out/inliner
101+
GEN out/libc.inc
102+
CC out/src/main.o
103+
LD out/shecc
104+
SHECC out/shecc-stage1.elf
105+
SHECC out/shecc-stage2.elf
89106
```
107+
108+
Run `make DYNLINK=1` to use the dynamic linking mode and generate the dynamically linked compiler:
109+
```shell
110+
# If using the dynamic linking mode, you should add 'DYNLINK=1' for each 'make' command.
111+
$ make DYNLINK=1
90112
CC+LD out/inliner
91113
GEN out/libc.inc
92114
CC out/src/main.o
93115
LD out/shecc
94116
SHECC out/shecc-stage1.elf
95117
SHECC out/shecc-stage2.elf
118+
119+
$ file out/shecc-stage2.elf
120+
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
96121
```
97122

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

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

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

115-
Example:
141+
Example 1: static linking mode
116142
```shell
117143
$ out/shecc -o fib tests/fib.c
118144
$ chmod +x fib
119145
$ qemu-arm fib
120146
```
121147

148+
Example 2: dynamic linking mode
149+
150+
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.
151+
```shell
152+
$ out/shecc --dynlink -o fib tests/fib.c
153+
$ chmod +x fib
154+
$ qemu-arm -L /usr/arm-linux-gnueabihf fib
155+
```
156+
122157
### IR Regression Tests
123158

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

143178
`shecc` comes with a comprehensive test suite (200+ test cases). To run the tests:
144179
```shell
180+
# Add 'DYNLINK=1' if using the dynamic linking mode.
145181
$ make check # Run all tests (stage 0 and stage 2)
146182
$ make check-stage0 # Test stage 0 compiler only
147183
$ make check-stage2 # Test stage 2 compiler only

0 commit comments

Comments
 (0)