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