Source file
test/nosplit.go
1
2
3
4
5
6
7
8
9 package main
10
11 import (
12 "bytes"
13 "fmt"
14 "io/ioutil"
15 "log"
16 "os"
17 "os/exec"
18 "path/filepath"
19 "regexp"
20 "runtime"
21 "strconv"
22 "strings"
23 )
24
25 const debug = false
26
27 var tests = `
28 # These are test cases for the linker analysis that detects chains of
29 # nosplit functions that would cause a stack overflow.
30 #
31 # Lines beginning with # are comments.
32 #
33 # Each test case describes a sequence of functions, one per line.
34 # Each function definition is the function name, then the frame size,
35 # then optionally the keyword 'nosplit', then the body of the function.
36 # The body is assembly code, with some shorthands.
37 # The shorthand 'call x' stands for CALL x(SB).
38 # The shorthand 'callind' stands for 'CALL R0', where R0 is a register.
39 # Each test case must define a function named start, and it must be first.
40 # That is, a line beginning "start " indicates the start of a new test case.
41 # Within a stanza, ; can be used instead of \n to separate lines.
42 #
43 # After the function definition, the test case ends with an optional
44 # REJECT line, specifying the architectures on which the case should
45 # be rejected. "REJECT" without any architectures means reject on all architectures.
46 # The linker should accept the test case on systems not explicitly rejected.
47 #
48 # 64-bit systems do not attempt to execute test cases with frame sizes
49 # that are only 32-bit aligned.
50
51 # Ordinary function should work
52 start 0
53
54 # Large frame marked nosplit is always wrong.
55 # Frame is so large it overflows cmd/link's int16.
56 start 100000 nosplit
57 REJECT
58
59 # Calling a large frame is okay.
60 start 0 call big
61 big 10000
62
63 # But not if the frame is nosplit.
64 start 0 call big
65 big 10000 nosplit
66 REJECT
67
68 # Recursion is okay.
69 start 0 call start
70
71 # Recursive nosplit runs out of space.
72 start 0 nosplit call start
73 REJECT
74
75 # Non-trivial recursion runs out of space.
76 start 0 call f1
77 f1 0 nosplit call f2
78 f2 0 nosplit call f1
79 REJECT
80 # Same but cycle starts below nosplit entry.
81 start 0 call f1
82 f1 0 nosplit call f2
83 f2 0 nosplit call f3
84 f3 0 nosplit call f2
85 REJECT
86
87 # Chains of ordinary functions okay.
88 start 0 call f1
89 f1 80 call f2
90 f2 80
91
92 # Chains of nosplit must fit in the stack limit, 128 bytes.
93 start 0 call f1
94 f1 80 nosplit call f2
95 f2 80 nosplit
96 REJECT
97
98 # Larger chains.
99 start 0 call f1
100 f1 16 call f2
101 f2 16 call f3
102 f3 16 call f4
103 f4 16 call f5
104 f5 16 call f6
105 f6 16 call f7
106 f7 16 call f8
107 f8 16 call end
108 end 1000
109
110 start 0 call f1
111 f1 16 nosplit call f2
112 f2 16 nosplit call f3
113 f3 16 nosplit call f4
114 f4 16 nosplit call f5
115 f5 16 nosplit call f6
116 f6 16 nosplit call f7
117 f7 16 nosplit call f8
118 f8 16 nosplit call end
119 end 1000
120 REJECT
121
122 # Two paths both go over the stack limit.
123 start 0 call f1
124 f1 80 nosplit call f2 call f3
125 f2 40 nosplit call f4
126 f3 96 nosplit
127 f4 40 nosplit
128 REJECT
129
130 # Test cases near the 128-byte limit.
131
132 # Ordinary stack split frame is always okay.
133 start 112
134 start 116
135 start 120
136 start 124
137 start 128
138 start 132
139 start 136
140
141 # A nosplit leaf can use the whole 128-CallSize bytes available on entry.
142 # (CallSize is 32 on ppc64, 8 on amd64 for frame pointer.)
143 start 96 nosplit
144 start 100 nosplit; REJECT ppc64 ppc64le
145 start 104 nosplit; REJECT ppc64 ppc64le arm64
146 start 108 nosplit; REJECT ppc64 ppc64le
147 start 112 nosplit; REJECT ppc64 ppc64le arm64
148 start 116 nosplit; REJECT ppc64 ppc64le
149 start 120 nosplit; REJECT ppc64 ppc64le amd64 arm64
150 start 124 nosplit; REJECT ppc64 ppc64le amd64
151 start 128 nosplit; REJECT
152 start 132 nosplit; REJECT
153 start 136 nosplit; REJECT
154
155 # Calling a nosplit function from a nosplit function requires
156 # having room for the saved caller PC and the called frame.
157 # Because ARM doesn't save LR in the leaf, it gets an extra 4 bytes.
158 # Because arm64 doesn't save LR in the leaf, it gets an extra 8 bytes.
159 # ppc64 doesn't save LR in the leaf, but CallSize is 32, so it gets 24 bytes.
160 # Because AMD64 uses frame pointer, it has 8 fewer bytes.
161 start 96 nosplit call f; f 0 nosplit
162 start 100 nosplit call f; f 0 nosplit; REJECT ppc64 ppc64le
163 start 104 nosplit call f; f 0 nosplit; REJECT ppc64 ppc64le arm64
164 start 108 nosplit call f; f 0 nosplit; REJECT ppc64 ppc64le
165 start 112 nosplit call f; f 0 nosplit; REJECT ppc64 ppc64le amd64 arm64
166 start 116 nosplit call f; f 0 nosplit; REJECT ppc64 ppc64le amd64
167 start 120 nosplit call f; f 0 nosplit; REJECT ppc64 ppc64le amd64 arm64
168 start 124 nosplit call f; f 0 nosplit; REJECT ppc64 ppc64le amd64 386
169 start 128 nosplit call f; f 0 nosplit; REJECT
170 start 132 nosplit call f; f 0 nosplit; REJECT
171 start 136 nosplit call f; f 0 nosplit; REJECT
172
173 # Calling a splitting function from a nosplit function requires
174 # having room for the saved caller PC of the call but also the
175 # saved caller PC for the call to morestack.
176 # Architectures differ in the same way as before.
177 start 96 nosplit call f; f 0 call f
178 start 100 nosplit call f; f 0 call f; REJECT ppc64 ppc64le
179 start 104 nosplit call f; f 0 call f; REJECT ppc64 ppc64le amd64 arm64
180 start 108 nosplit call f; f 0 call f; REJECT ppc64 ppc64le amd64
181 start 112 nosplit call f; f 0 call f; REJECT ppc64 ppc64le amd64 arm64
182 start 116 nosplit call f; f 0 call f; REJECT ppc64 ppc64le amd64
183 start 120 nosplit call f; f 0 call f; REJECT ppc64 ppc64le amd64 386 arm64
184 start 124 nosplit call f; f 0 call f; REJECT ppc64 ppc64le amd64 386
185 start 128 nosplit call f; f 0 call f; REJECT
186 start 132 nosplit call f; f 0 call f; REJECT
187 start 136 nosplit call f; f 0 call f; REJECT
188
189 # Indirect calls are assumed to be splitting functions.
190 start 96 nosplit callind
191 start 100 nosplit callind; REJECT ppc64 ppc64le
192 start 104 nosplit callind; REJECT ppc64 ppc64le amd64 arm64
193 start 108 nosplit callind; REJECT ppc64 ppc64le amd64
194 start 112 nosplit callind; REJECT ppc64 ppc64le amd64 arm64
195 start 116 nosplit callind; REJECT ppc64 ppc64le amd64
196 start 120 nosplit callind; REJECT ppc64 ppc64le amd64 386 arm64
197 start 124 nosplit callind; REJECT ppc64 ppc64le amd64 386
198 start 128 nosplit callind; REJECT
199 start 132 nosplit callind; REJECT
200 start 136 nosplit callind; REJECT
201
202 # Issue 7623
203 start 0 call f; f 112
204 start 0 call f; f 116
205 start 0 call f; f 120
206 start 0 call f; f 124
207 start 0 call f; f 128
208 start 0 call f; f 132
209 start 0 call f; f 136
210 `
211
212 var (
213 commentRE = regexp.MustCompile(`(?m)^#.*`)
214 rejectRE = regexp.MustCompile(`(?s)\A(.+?)((\n|; *)REJECT(.*))?\z`)
215 lineRE = regexp.MustCompile(`(\w+) (\d+)( nosplit)?(.*)`)
216 callRE = regexp.MustCompile(`\bcall (\w+)\b`)
217 callindRE = regexp.MustCompile(`\bcallind\b`)
218 )
219
220 func main() {
221 goarch := os.Getenv("GOARCH")
222 if goarch == "" {
223 goarch = runtime.GOARCH
224 }
225
226 dir, err := ioutil.TempDir("", "go-test-nosplit")
227 if err != nil {
228 bug()
229 fmt.Printf("creating temp dir: %v\n", err)
230 return
231 }
232 defer os.RemoveAll(dir)
233 os.Setenv("GOPATH", filepath.Join(dir, "_gopath"))
234
235 if err := ioutil.WriteFile(filepath.Join(dir, "go.mod"), []byte("module go-test-nosplit\n"), 0666); err != nil {
236 log.Panic(err)
237 }
238
239 tests = strings.Replace(tests, "\t", " ", -1)
240 tests = commentRE.ReplaceAllString(tests, "")
241
242 nok := 0
243 nfail := 0
244 TestCases:
245 for len(tests) > 0 {
246 var stanza string
247 i := strings.Index(tests, "\nstart ")
248 if i < 0 {
249 stanza, tests = tests, ""
250 } else {
251 stanza, tests = tests[:i], tests[i+1:]
252 }
253
254 m := rejectRE.FindStringSubmatch(stanza)
255 if m == nil {
256 bug()
257 fmt.Printf("invalid stanza:\n\t%s\n", indent(stanza))
258 continue
259 }
260 lines := strings.TrimSpace(m[1])
261 reject := false
262 if m[2] != "" {
263 if strings.TrimSpace(m[4]) == "" {
264 reject = true
265 } else {
266 for _, rej := range strings.Fields(m[4]) {
267 if rej == goarch {
268 reject = true
269 }
270 }
271 }
272 }
273 if lines == "" && !reject {
274 continue
275 }
276
277 var gobuf bytes.Buffer
278 fmt.Fprintf(&gobuf, "package main\n")
279
280 var buf bytes.Buffer
281 ptrSize := 4
282 switch goarch {
283 case "mips", "mipsle":
284 fmt.Fprintf(&buf, "#define REGISTER (R0)\n")
285 case "mips64", "mips64le":
286 ptrSize = 8
287 fmt.Fprintf(&buf, "#define REGISTER (R0)\n")
288 case "loong64":
289 ptrSize = 8
290 fmt.Fprintf(&buf, "#define REGISTER (R0)\n")
291 case "ppc64", "ppc64le":
292 ptrSize = 8
293 fmt.Fprintf(&buf, "#define REGISTER (CTR)\n")
294 case "arm":
295 fmt.Fprintf(&buf, "#define REGISTER (R0)\n")
296 case "arm64":
297 ptrSize = 8
298 fmt.Fprintf(&buf, "#define REGISTER (R0)\n")
299 case "amd64":
300 ptrSize = 8
301 fmt.Fprintf(&buf, "#define REGISTER AX\n")
302 case "riscv64":
303 ptrSize = 8
304 fmt.Fprintf(&buf, "#define REGISTER A0\n")
305 case "s390x":
306 ptrSize = 8
307 fmt.Fprintf(&buf, "#define REGISTER R10\n")
308 default:
309 fmt.Fprintf(&buf, "#define REGISTER AX\n")
310 }
311
312
313
314
315
316 fmt.Fprintf(&gobuf, "func main0()\n")
317 fmt.Fprintf(&gobuf, "func main() { main0() }\n")
318 fmt.Fprintf(&buf, "TEXT ·main0(SB),0,$0-0\n\tCALL ·start(SB)\n")
319
320 adjusted := false
321 for _, line := range strings.Split(lines, "\n") {
322 line = strings.TrimSpace(line)
323 if line == "" {
324 continue
325 }
326 for _, subline := range strings.Split(line, ";") {
327 subline = strings.TrimSpace(subline)
328 if subline == "" {
329 continue
330 }
331 m := lineRE.FindStringSubmatch(subline)
332 if m == nil {
333 bug()
334 fmt.Printf("invalid function line: %s\n", subline)
335 continue TestCases
336 }
337 name := m[1]
338 size, _ := strconv.Atoi(m[2])
339
340 if size%ptrSize == 4 {
341 continue TestCases
342 }
343 nosplit := m[3]
344 body := m[4]
345
346
347
348
349
350
351 if !adjusted && nosplit != "" {
352 const stackNosplitBase = 800
353 adjusted = true
354 size += stackNosplitBase - 128
355 }
356
357 if nosplit != "" {
358 nosplit = ",7"
359 } else {
360 nosplit = ",0"
361 }
362 body = callRE.ReplaceAllString(body, "CALL ·$1(SB);")
363 body = callindRE.ReplaceAllString(body, "CALL REGISTER;")
364
365 fmt.Fprintf(&gobuf, "func %s()\n", name)
366 fmt.Fprintf(&buf, "TEXT ·%s(SB)%s,$%d-0\n\t%s\n\tRET\n\n", name, nosplit, size, body)
367 }
368 }
369
370 if debug {
371 fmt.Printf("===\n%s\n", strings.TrimSpace(stanza))
372 fmt.Printf("-- main.go --\n%s", gobuf.String())
373 fmt.Printf("-- asm.s --\n%s", buf.String())
374 }
375
376 if err := ioutil.WriteFile(filepath.Join(dir, "asm.s"), buf.Bytes(), 0666); err != nil {
377 log.Fatal(err)
378 }
379 if err := ioutil.WriteFile(filepath.Join(dir, "main.go"), gobuf.Bytes(), 0666); err != nil {
380 log.Fatal(err)
381 }
382
383 cmd := exec.Command("go", "build")
384 cmd.Dir = dir
385 output, err := cmd.CombinedOutput()
386 if err == nil {
387 nok++
388 if reject {
389 bug()
390 fmt.Printf("accepted incorrectly:\n\t%s\n", indent(strings.TrimSpace(stanza)))
391 }
392 } else {
393 nfail++
394 if !reject {
395 bug()
396 fmt.Printf("rejected incorrectly:\n\t%s\n", indent(strings.TrimSpace(stanza)))
397 fmt.Printf("\n\tlinker output:\n\t%s\n", indent(string(output)))
398 }
399 }
400 }
401
402 if !bugged && (nok == 0 || nfail == 0) {
403 bug()
404 fmt.Printf("not enough test cases run\n")
405 }
406 }
407
408 func indent(s string) string {
409 return strings.Replace(s, "\n", "\n\t", -1)
410 }
411
412 var bugged = false
413
414 func bug() {
415 if !bugged {
416 bugged = true
417 fmt.Printf("BUG\n")
418 }
419 }
420
View as plain text