Skip to content

Commit aad42ea

Browse files
authored
Merge pull request #1435 from luoliwoshang/fix/esp32c3println
fix(esp32c3): use newlib startup and merge init_array to rodata for println support
2 parents 0233891 + 0f559dc commit aad42ea

File tree

8 files changed

+308
-39
lines changed

8 files changed

+308
-39
lines changed

.github/workflows/llgo.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,14 @@ jobs:
140140
chmod +x verify_export.sh
141141
./verify_export.sh
142142
143+
- name: Test ESP32-C3 newlib startup
144+
run: |
145+
echo "Testing ESP32-C3 uses newlib startup (not TinyGo start.S)..."
146+
pip3 install --break-system-packages esptool==5.1.0
147+
cd _demo/embed
148+
chmod +x test_esp32c3_startup.sh
149+
./test_esp32c3_startup.sh
150+
143151
- name: _xtool build tests
144152
run: |
145153
cd _xtool
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
#!/bin/bash
2+
# ESP32-C3 Startup and .init_array Regression Test
3+
#
4+
# Verifies:
5+
# 1. _start uses newlib's __libc_init_array (not TinyGo's start.S)
6+
# 2. .init_array section is merged into .rodata section
7+
# 3. .rodata (including .init_array) is included in BIN file
8+
9+
set -e
10+
11+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12+
TEMP_DIR=$(mktemp -d)
13+
TEST_GO="$TEMP_DIR/main.go"
14+
TEST_ELF="test.elf"
15+
TEST_BIN="test.bin"
16+
17+
cleanup() {
18+
rm -rf "$TEMP_DIR"
19+
}
20+
trap cleanup EXIT
21+
22+
# Check if esptool.py is installed
23+
# esptool.py is required to parse ESP32-C3 BIN file format and verify
24+
# that .rodata segment (containing .init_array) is included in the firmware
25+
if ! command -v esptool.py &> /dev/null; then
26+
echo "✗ FAIL: esptool.py not found"
27+
echo "Please install: pip3 install esptool==5.1.0"
28+
exit 1
29+
fi
30+
31+
echo "==> Creating minimal test program..."
32+
cat > "$TEST_GO" << 'EOF'
33+
package main
34+
35+
func main() {}
36+
EOF
37+
38+
echo "==> Building for ESP32-C3 target (ELF + BIN)..."
39+
cd "$TEMP_DIR"
40+
llgo build -target=esp32c3 -o test -obin "$TEST_GO"
41+
42+
if [ ! -f "$TEST_ELF" ]; then
43+
echo "✗ FAIL: Build failed, $TEST_ELF not found"
44+
exit 1
45+
fi
46+
47+
if [ ! -f "$TEST_BIN" ]; then
48+
echo "✗ FAIL: BIN file not generated, $TEST_BIN not found"
49+
exit 1
50+
fi
51+
52+
echo ""
53+
echo "=== Test 1: Verify newlib startup (calls __libc_init_array) ==="
54+
55+
# Disassemble _start and check for __libc_init_array call
56+
if llvm-objdump -d "$TEST_ELF" | grep -A50 "<_start>:" | grep "__libc_init_array" > /dev/null; then
57+
echo "✓ PASS: _start calls __libc_init_array"
58+
echo " ESP32-C3 uses newlib's standard startup"
59+
else
60+
echo "✗ FAIL: _start does NOT call __libc_init_array"
61+
echo " ESP32-C3 should use newlib's startup flow"
62+
echo ""
63+
echo "Expected inheritance: esp32c3 → riscv32-nostart → riscv-nostart → riscv-basic"
64+
echo "Current _start disassembly:"
65+
llvm-objdump -d "$TEST_ELF" | grep -A50 "<_start>:" || true
66+
exit 1
67+
fi
68+
69+
echo ""
70+
echo "=== Test 2: Verify .init_array merged into .rodata (ELF) ==="
71+
72+
# Get .rodata section information from ELF file
73+
# The .rodata section should contain the merged .init_array data
74+
#
75+
# Real output from: llvm-readelf -S "$TEST_ELF"
76+
#
77+
# Section Headers:
78+
# [Nr] Name Type Address Off Size ES Flg Lk Inf Al
79+
# [ 2] .rodata PROGBITS 4038042c 00142c 00003c 00 WAMS 0 0 4
80+
# | | | | | |
81+
# $1 $2 $4 $5 $6 $7
82+
# [ 2] PROGBITS 4038042c 00142c 00003c
83+
# (section number) (type) (ADDR) (off) (SIZE)
84+
#
85+
# IMPORTANT: Note the space between '[' and '2]' - they are TWO separate fields!
86+
#
87+
# Field breakdown (awk splits on whitespace):
88+
# $1 = [ (opening bracket - yes, it's a separate field!)
89+
# $2 = 2] (section number with closing bracket)
90+
# $3 = .rodata (section name)
91+
# $4 = PROGBITS (section type)
92+
# $5 = 4038042c (address - THIS IS WHAT WE EXTRACT for RODATA_ADDR)
93+
# $6 = 00142c (file offset)
94+
# $7 = 00003c (size - THIS IS WHAT WE EXTRACT for RODATA_SIZE)
95+
# $8 = 00 (entry size)
96+
#
97+
# We use: awk '{print "0x"$5}' to extract address (4038042c → 0x4038042c)
98+
# awk '{print "0x"$7}' to extract size (00003c → 0x00003c)
99+
RODATA_INFO=$(llvm-readelf -S "$TEST_ELF" | grep "\.rodata")
100+
if [ -z "$RODATA_INFO" ]; then
101+
echo "✗ FAIL: .rodata section not found"
102+
exit 1
103+
fi
104+
105+
RODATA_ADDR=$(echo "$RODATA_INFO" | awk '{print "0x"$5}')
106+
RODATA_SIZE=$(echo "$RODATA_INFO" | awk '{print "0x"$7}')
107+
RODATA_END=$(printf "0x%x" $((RODATA_ADDR + RODATA_SIZE)))
108+
109+
echo ".rodata section: start=$RODATA_ADDR, size=$RODATA_SIZE, end=$RODATA_END"
110+
111+
# Get __init_array_start symbol address
112+
# This symbol marks where .init_array was placed by the linker script
113+
# It should point to an address within the .rodata section
114+
#
115+
# Real output from: llvm-nm "$TEST_ELF"
116+
#
117+
# 40380450 d __init_array_start
118+
# ^$1 ^$2 ^$3
119+
# (Addr) (Type: d=local data symbol in .rodata)
120+
#
121+
# Field breakdown:
122+
# $1 = 40380450 (symbol address in hex without 0x prefix)
123+
# $2 = d (symbol type: d=local data, D=global data, T=text, etc.)
124+
# $3 = __init_array_start (symbol name)
125+
#
126+
# We extract $1 and add "0x" prefix: 40380450 → 0x40380450
127+
INIT_ARRAY_START=$(llvm-nm "$TEST_ELF" | grep "__init_array_start" | awk '{print "0x"$1}')
128+
if [ -z "$INIT_ARRAY_START" ]; then
129+
echo "✗ FAIL: __init_array_start symbol not found"
130+
exit 1
131+
fi
132+
133+
echo "__init_array_start: $INIT_ARRAY_START"
134+
135+
# Verify that __init_array_start address falls within .rodata section bounds
136+
# This confirms the linker script successfully merged .init_array into .rodata
137+
INIT_ADDR_DEC=$((INIT_ARRAY_START))
138+
RODATA_ADDR_DEC=$((RODATA_ADDR))
139+
RODATA_END_DEC=$((RODATA_END))
140+
141+
if [ $INIT_ADDR_DEC -ge $RODATA_ADDR_DEC ] && [ $INIT_ADDR_DEC -lt $RODATA_END_DEC ]; then
142+
OFFSET=$((INIT_ADDR_DEC - RODATA_ADDR_DEC))
143+
echo "✓ PASS: __init_array_start is within .rodata (offset: +0x$(printf '%x' $OFFSET))"
144+
else
145+
echo "✗ FAIL: __init_array_start is NOT within .rodata"
146+
echo " .rodata: [$RODATA_ADDR, $RODATA_END)"
147+
echo " __init_array_start: $INIT_ARRAY_START"
148+
exit 1
149+
fi
150+
151+
echo ""
152+
echo "=== Test 3: Verify .rodata included in BIN file ==="
153+
154+
# Get BIN file segment information using esptool.py
155+
# ESP32-C3 BIN files contain multiple segments with load addresses
156+
# We need to verify that .rodata segment exists in the BIN file
157+
#
158+
# Real output from: esptool.py --chip esp32c3 image_info test.bin
159+
#
160+
# Segments Information
161+
# ====================
162+
# Segment Length Load addr File offs Memory types
163+
# ------- ------- ---------- ---------- ------------
164+
# 0 0x000f0 0x3fc84468 0x00000018 DRAM
165+
# 1 0x00004 0x3fc84558 0x00000110 DRAM
166+
# 2 0x0006c 0x3fc8455c 0x0000011c DRAM
167+
# 3 0x0042c 0x40380000 0x00000190 IRAM
168+
# 4 0x0003c 0x4038042c 0x000005c4 IRAM ← This is .rodata!
169+
# ^$2 ^$3 ^$4
170+
# (Length) (LoadAddr) (FileOffset)
171+
#
172+
# Field breakdown for segment line:
173+
# $1 = 4 (segment number)
174+
# $2 = 0x0003c (segment length/size)
175+
# $3 = 0x4038042c (load address - matches .rodata address from ELF!)
176+
# $4 = 0x000005c4 (file offset in BIN)
177+
# $5+ = IRAM (memory type)
178+
#
179+
# We extract $2 (length) and $3 (load addr) to verify __init_array_start is within bounds
180+
if ! BIN_INFO=$(esptool.py --chip esp32c3 image_info "$TEST_BIN" 2>&1); then
181+
echo "✗ FAIL: esptool.py failed to parse BIN file"
182+
echo "$BIN_INFO"
183+
exit 1
184+
fi
185+
186+
# Find the segment containing .rodata by matching its load address
187+
# Strip leading "0x" and zeros from address for grepping
188+
RODATA_ADDR_CLEAN=$(echo "$RODATA_ADDR" | sed 's/^0x0*//')
189+
RODATA_SEG=$(echo "$BIN_INFO" | grep -i "$RODATA_ADDR_CLEAN")
190+
191+
if [ -z "$RODATA_SEG" ]; then
192+
echo "✗ FAIL: .rodata segment not found in BIN file"
193+
echo "Looking for address: $RODATA_ADDR"
194+
echo ""
195+
echo "BIN segments:"
196+
echo "$BIN_INFO" | grep -A20 "Segments Information"
197+
exit 1
198+
fi
199+
200+
# Extract segment information from esptool.py output
201+
# Format: Segment_Number Length Load_Address ...
202+
# We parse these three fields to determine segment boundaries
203+
SEG_NUM=$(echo "$RODATA_SEG" | awk '{print $1}')
204+
SEG_LEN=$(echo "$RODATA_SEG" | awk '{print $2}')
205+
SEG_LOAD=$(echo "$RODATA_SEG" | awk '{print $3}')
206+
207+
SEG_LEN_DEC=$((SEG_LEN))
208+
SEG_LOAD_DEC=$((SEG_LOAD))
209+
SEG_END=$((SEG_LOAD_DEC + SEG_LEN_DEC))
210+
211+
echo "BIN Segment $SEG_NUM: start=$SEG_LOAD, length=$SEG_LEN, end=$(printf '0x%x' $SEG_END)"
212+
213+
# Verify that __init_array_start falls within this BIN segment
214+
# This confirms that .init_array data (merged into .rodata) will be
215+
# correctly flashed to ESP32-C3 and available at runtime
216+
if [ $INIT_ADDR_DEC -ge $SEG_LOAD_DEC ] && [ $INIT_ADDR_DEC -lt $SEG_END ]; then
217+
OFFSET=$((INIT_ADDR_DEC - SEG_LOAD_DEC))
218+
echo "✓ PASS: __init_array_start is within BIN Segment $SEG_NUM (offset: +0x$(printf '%x' $OFFSET))"
219+
else
220+
echo "✗ FAIL: __init_array_start is NOT within BIN segment"
221+
echo " Segment range: [$SEG_LOAD, $(printf '0x%x' $SEG_END))"
222+
echo " __init_array_start: $INIT_ARRAY_START"
223+
exit 1
224+
fi
225+
226+
echo ""
227+
echo "=== All Tests Passed ==="
228+
echo "✓ ESP32-C3 uses newlib startup (_start calls __libc_init_array)"
229+
echo "✓ .init_array merged into .rodata section"
230+
echo "✓ .rodata (including .init_array) included in BIN file"
231+
echo "✓ Constructor function pointers will be correctly flashed to ESP32-C3"
232+
233+
exit 0

targets/esp32-riscv.app.elf.ld

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,39 +28,41 @@ SECTIONS
2828
*(.rodata1)
2929
*(.sdata2 .sdata2.* .gnu.linkonce.s2.*)
3030
*(.sbss2 .sbss2.* .gnu.linkonce.sb2.*)
31-
. = ALIGN(4);
32-
__cpu_frequency = .;
33-
LONG(CPU_FREQUENCY);
34-
__uart0_clkdiv_reg = .;
35-
LONG(UART0_CLKDIV_REG);
36-
__uart0_clkdiv_val = .;
37-
LONG(UART0_CLKDIV_VAL);
38-
__uart0_tx_addr = .;
39-
LONG(UART0_TX_ADDR);
40-
__uart0_status = .;
41-
LONG(UART0_STATUS);
42-
} > iram_seg
4331

44-
.preinit_array :
45-
{
32+
/* note(zzy): .preinit_array, .init_array and .fini_array were originally separate sections,
33+
* but they get lost during ELF→BIN conversion. Following ESP-IDF's approach, merge them
34+
* into .rodata for proper loading.
35+
*/
36+
. = ALIGN(4);
4637
PROVIDE_HIDDEN (__preinit_array_start = .);
4738
KEEP (*(.preinit_array))
4839
PROVIDE_HIDDEN (__preinit_array_end = .);
49-
} > iram_seg
50-
.init_array :
51-
{
40+
41+
. = ALIGN(4);
5242
PROVIDE_HIDDEN (__init_array_start = .);
5343
KEEP (*(SORT_BY_INIT_PRIORITY(.init_array.*) SORT_BY_INIT_PRIORITY(.ctors.*)))
5444
KEEP (*(.init_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .ctors))
5545
PROVIDE_HIDDEN (__init_array_end = .);
56-
} > iram_seg
57-
.fini_array :
58-
{
46+
47+
. = ALIGN(4);
5948
PROVIDE_HIDDEN (__fini_array_start = .);
6049
KEEP (*(SORT_BY_INIT_PRIORITY(.fini_array.*) SORT_BY_INIT_PRIORITY(.dtors.*)))
6150
KEEP (*(.fini_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .dtors))
6251
PROVIDE_HIDDEN (__fini_array_end = .);
52+
53+
. = ALIGN(4);
54+
__cpu_frequency = .;
55+
LONG(CPU_FREQUENCY);
56+
__uart0_clkdiv_reg = .;
57+
LONG(UART0_CLKDIV_REG);
58+
__uart0_clkdiv_val = .;
59+
LONG(UART0_CLKDIV_VAL);
60+
__uart0_tx_addr = .;
61+
LONG(UART0_TX_ADDR);
62+
__uart0_status = .;
63+
LONG(UART0_STATUS);
6364
} > iram_seg
65+
6466
.ctors :
6567
{
6668
/* gcc uses crtbegin.o to find the start of
@@ -94,6 +96,7 @@ SECTIONS
9496

9597
.stack (NOLOAD) :
9698
{
99+
. = ALIGN(16);
97100
. += 16K;
98101
__stack = .;
99102
} > dram_seg

targets/esp32c3.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"inherits": [
3-
"riscv32"
3+
"riscv32-nostart"
44
],
55
"features": "+32bit,+c,+m,+zmmul,-a,-b,-d,-e,-experimental-smmpm,-experimental-smnpm,-experimental-ssnpm,-experimental-sspm,-experimental-ssqosid,-experimental-supm,-experimental-zacas,-experimental-zalasr,-experimental-zicfilp,-experimental-zicfiss,-f,-h,-relax,-shcounterenw,-shgatpa,-shtvala,-shvsatpa,-shvstvala,-shvstvecd,-smaia,-smcdeleg,-smcsrind,-smepmp,-smstateen,-ssaia,-ssccfg,-ssccptr,-sscofpmf,-sscounterenw,-sscsrind,-ssstateen,-ssstrict,-sstc,-sstvala,-sstvecd,-ssu64xl,-svade,-svadu,-svbare,-svinval,-svnapot,-svpbmt,-v,-xcvalu,-xcvbi,-xcvbitmanip,-xcvelw,-xcvmac,-xcvmem,-xcvsimd,-xesppie,-xsfcease,-xsfvcp,-xsfvfnrclipxfqf,-xsfvfwmaccqqq,-xsfvqmaccdod,-xsfvqmaccqoq,-xsifivecdiscarddlone,-xsifivecflushdlone,-xtheadba,-xtheadbb,-xtheadbs,-xtheadcmo,-xtheadcondmov,-xtheadfmemidx,-xtheadmac,-xtheadmemidx,-xtheadmempair,-xtheadsync,-xtheadvdot,-xventanacondops,-xwchc,-za128rs,-za64rs,-zaamo,-zabha,-zalrsc,-zama16b,-zawrs,-zba,-zbb,-zbc,-zbkb,-zbkc,-zbkx,-zbs,-zca,-zcb,-zcd,-zce,-zcf,-zcmop,-zcmp,-zcmt,-zdinx,-zfa,-zfbfmin,-zfh,-zfhmin,-zfinx,-zhinx,-zhinxmin,-zic64b,-zicbom,-zicbop,-zicboz,-ziccamoa,-ziccif,-zicclsm,-ziccrse,-zicntr,-zicond,-zicsr,-zifencei,-zihintntl,-zihintpause,-zihpm,-zimop,-zk,-zkn,-zknd,-zkne,-zknh,-zkr,-zks,-zksed,-zksh,-zkt,-ztso,-zvbb,-zvbc,-zve32f,-zve32x,-zve64d,-zve64f,-zve64x,-zvfbfmin,-zvfbfwma,-zvfh,-zvfhmin,-zvkb,-zvkg,-zvkn,-zvknc,-zvkned,-zvkng,-zvknha,-zvknhb,-zvks,-zvksc,-zvksed,-zvksg,-zvksh,-zvkt,-zvl1024b,-zvl128b,-zvl16384b,-zvl2048b,-zvl256b,-zvl32768b,-zvl32b,-zvl4096b,-zvl512b,-zvl64b,-zvl65536b,-zvl8192b",
66
"build-tags": [

targets/riscv-basic.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"goos": "linux",
3+
"goarch": "arm",
4+
"build-tags": ["tinygo.riscv", "baremetal", "linux", "arm"],
5+
"gc": "conservative",
6+
"linker": "ld.lld",
7+
"rtlib": "compiler-rt",
8+
"libc": "picolibc",
9+
"cflags": [
10+
"-Werror",
11+
"-mno-relax",
12+
"-fno-exceptions", "-fno-unwind-tables", "-fno-asynchronous-unwind-tables",
13+
"-ffunction-sections", "-fdata-sections"
14+
],
15+
"ldflags": [
16+
"--gc-sections"
17+
],
18+
"gdb": ["riscv64-unknown-elf-gdb"]
19+
}

targets/riscv-nostart.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"inherits": ["riscv-basic"]
3+
}

targets/riscv.json

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,7 @@
11
{
2-
"goos": "linux",
3-
"goarch": "arm",
4-
"build-tags": ["tinygo.riscv", "baremetal", "linux", "arm"],
5-
"gc": "conservative",
6-
"linker": "ld.lld",
7-
"rtlib": "compiler-rt",
8-
"libc": "picolibc",
9-
"cflags": [
10-
"-Werror",
11-
"-mno-relax",
12-
"-fno-exceptions", "-fno-unwind-tables", "-fno-asynchronous-unwind-tables",
13-
"-ffunction-sections", "-fdata-sections"
14-
],
15-
"ldflags": [
16-
"--gc-sections"
17-
],
2+
"inherits": ["riscv-basic"],
183
"extra-files": [
194
"targets/device/riscv/start.S",
205
"targets/device/riscv/handleinterrupt.S"
21-
],
22-
"gdb": ["riscv64-unknown-elf-gdb"]
6+
]
237
}

targets/riscv32-nostart.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"inherits": ["riscv-nostart"],
3+
"llvm-target": "riscv32-unknown-none",
4+
"cpu": "generic-rv32",
5+
"target-abi": "ilp32",
6+
"build-tags": ["tinygo.riscv32"],
7+
"scheduler": "tasks",
8+
"default-stack-size": 2048,
9+
"cflags": [
10+
"-march=rv32imac"
11+
],
12+
"ldflags": [
13+
"-melf32lriscv"
14+
],
15+
"gdb": [
16+
"gdb-multiarch",
17+
"gdb"
18+
]
19+
}

0 commit comments

Comments
 (0)