1#!/usr/bin/env python3
2#
3# Copyright (c) 2017 Intel Corporation
4#
5# SPDX-License-Identifier: Apache-2.0
6
7"""
8Script to generate system call invocation macros
9
10This script parses the system call metadata JSON file emitted by
11parse_syscalls.py to create several files:
12
13- A file containing weak aliases of any potentially unimplemented system calls,
14  as well as the system call dispatch table, which maps system call type IDs
15  to their handler functions.
16
17- A header file defining the system call type IDs, as well as function
18  prototypes for all system call handler functions.
19
20- A directory containing header files. Each header corresponds to a header
21  that was identified as containing system call declarations. These
22  generated headers contain the inline invocation functions for each system
23  call in that header.
24"""
25
26import argparse
27import json
28import os
29import re
30import sys
31
32# Some kernel headers cannot include automated tracing without causing unintended recursion or
33# other serious issues.
34# These headers typically already have very specific tracing hooks for all relevant things
35# written by hand so are excluded.
36notracing = ["kernel.h", "zephyr/kernel.h", "errno_private.h", "zephyr/errno_private.h"]
37
38types64 = ["int64_t", "uint64_t"]
39
40# The kernel linkage is complicated.  These functions from
41# userspace_handlers.c are present in the kernel .a library after
42# userspace.c, which contains the weak fallbacks defined here.  So the
43# linker finds the weak one first and stops searching, and thus won't
44# see the real implementation which should override.  Yet changing the
45# order runs afoul of a comment in CMakeLists.txt that the order is
46# critical.  These are core syscalls that won't ever be unconfigured,
47# just disable the fallback mechanism as a simple workaround.
48noweak = ["z_mrsh_k_object_release", "z_mrsh_k_object_access_grant", "z_mrsh_k_object_alloc"]
49
50table_template = """/* auto-generated by gen_syscalls.py, don't edit */
51
52#include <zephyr/llext/symbol.h>
53
54/* Weak handler functions that get replaced by the real ones unless a system
55 * call is not implemented due to kernel configuration.
56 */
57%s
58
59const _k_syscall_handler_t _k_syscall_table[K_SYSCALL_LIMIT] = {
60\t%s
61};
62"""
63
64list_template = """/* auto-generated by gen_syscalls.py, don't edit */
65
66#ifndef ZEPHYR_SYSCALL_LIST_H
67#define ZEPHYR_SYSCALL_LIST_H
68
69%s
70
71#ifndef _ASMLANGUAGE
72
73#include <stdarg.h>
74#include <stdint.h>
75
76#endif /* _ASMLANGUAGE */
77
78#endif /* ZEPHYR_SYSCALL_LIST_H */
79"""
80
81syscall_template = """/* auto-generated by gen_syscalls.py, don't edit */
82
83{include_guard}
84
85{tracing_include}
86
87#ifndef _ASMLANGUAGE
88
89#include <stdarg.h>
90
91#include <zephyr/syscall_list.h>
92#include <zephyr/syscall.h>
93
94#include <zephyr/linker/sections.h>
95
96
97#ifdef __cplusplus
98extern "C" {{
99#endif
100
101{invocations}
102
103#ifdef __cplusplus
104}}
105#endif
106
107#endif
108#endif /* include guard */
109"""
110
111handler_template = """
112extern uintptr_t z_hdlr_%s(uintptr_t arg1, uintptr_t arg2, uintptr_t arg3,
113                uintptr_t arg4, uintptr_t arg5, uintptr_t arg6, void *ssf);
114"""
115
116weak_template = """
117__weak ALIAS_OF(handler_no_syscall)
118uintptr_t %s(uintptr_t arg1, uintptr_t arg2, uintptr_t arg3,
119         uintptr_t arg4, uintptr_t arg5, uintptr_t arg6, void *ssf);
120"""
121
122# defines a macro wrapper which supersedes the syscall when used
123# and provides tracing enter/exit hooks while allowing per compilation unit
124# enable/disable of syscall tracing. Used for returning functions
125# Note that the last argument to the exit macro is the return value.
126syscall_tracer_with_return_template = """
127#if defined(CONFIG_TRACING_SYSCALL)
128#ifndef DISABLE_SYSCALL_TRACING
129{trace_diagnostic}
130#define {func_name}({argnames}) ({{ \
131	{func_type} syscall__retval; \
132	sys_port_trace_syscall_enter({syscall_id}, {func_name}{trace_argnames}); \
133	syscall__retval = {func_name}({argnames}); \
134	sys_port_trace_syscall_exit({syscall_id}, {func_name}{trace_argnames}, syscall__retval); \
135	syscall__retval; \
136}})
137#endif
138#endif
139"""
140
141# defines a macro wrapper which supersedes the syscall when used
142# and provides tracing enter/exit hooks while allowing per compilation unit
143# enable/disable of syscall tracing. Used for non-returning (void) functions
144syscall_tracer_void_template = """
145#if defined(CONFIG_TRACING_SYSCALL)
146#ifndef DISABLE_SYSCALL_TRACING
147{trace_diagnostic}
148#define {func_name}({argnames}) do {{ \
149	sys_port_trace_syscall_enter({syscall_id}, {func_name}{trace_argnames}); \
150	{func_name}({argnames}); \
151	sys_port_trace_syscall_exit({syscall_id}, {func_name}{trace_argnames}); \
152}} while(false)
153#endif
154#endif
155"""
156
157
158llext_weakdefs_template = """/* auto-generated by gen_syscalls.py, don't edit */
159
160#include <zephyr/toolchain.h>
161#include <zephyr/llext/symbol.h>
162
163/*
164 * This symbol is placed at address 0 by llext-sections.ld. Its value and
165 * type is not important, we are only interested in its location.
166 */
167static void * const no_syscall_impl Z_GENERIC_SECTION(llext_no_syscall_impl);
168
169/*
170 * Weak references to all syscall implementations. Those not found by the
171 * linker outside this file will be exported as NULL and simply fail when
172 * an extension requiring them is loaded.
173 */
174%s
175"""
176
177
178llext_exports_template = """/* auto-generated by gen_syscalls.py, don't edit */
179
180/*
181 * Export the implementation functions of all emitted syscalls.
182 * Only the symbol names are relevant in this file, they will be
183 * resolved to the actual implementation functions by the linker.
184 */
185
186/* Symbol declarations */
187%s
188
189/* Exported symbols */
190%s
191"""
192
193typename_regex = re.compile(r'(.*?)([A-Za-z0-9_]+)$')
194
195
196class SyscallParseException(Exception):
197    pass
198
199
200def typename_split(item):
201    item = item.strip().replace("\n", " ")
202    if "[" in item:
203        raise SyscallParseException(
204            f"Please pass arrays to syscalls as pointers, unable to process '{item}'"
205        )
206
207    if "(" in item:
208        raise SyscallParseException("Please use typedefs for function pointers")
209
210    mo = typename_regex.match(item)
211    if not mo:
212        raise SyscallParseException("Malformed system call invocation")
213
214    m = mo.groups()
215    return (m[0].strip(), m[1])
216
217
218def need_split(argtype):
219    return (not args.long_registers) and (argtype in types64)
220
221
222# Note: "lo" and "hi" are named in little endian conventions,
223# but it doesn't matter as long as they are consistently
224# generated.
225def union_decl(ctype, split):
226    middle = "struct { uintptr_t lo, hi; } split" if split else "uintptr_t x"
227    return f"union {{ {middle}; {ctype} val; }}"
228
229
230def wrapper_defs(func_name, func_type, args, fn, userspace_only):
231    ret64 = need_split(func_type)
232    mrsh_args = []  # List of rvalue expressions for the marshalled invocation
233
234    decl_arglist = ", ".join([" ".join(argrec) for argrec in args]) or "void"
235    syscall_id = "K_SYSCALL_" + func_name.upper()
236
237    wrap = ''
238    if not userspace_only:
239        wrap += f"extern {func_type} z_impl_{func_name}({decl_arglist});\n"
240        wrap += "\n"
241
242    wrap += "__pinned_func\n"
243    wrap += f"static inline {func_type} {func_name}({decl_arglist})\n"
244    wrap += "{\n"
245    if not userspace_only:
246        wrap += "#ifdef CONFIG_USERSPACE\n"
247
248    wrap += ("\t" + "uint64_t ret64;\n") if ret64 else ""
249    if not userspace_only:
250        wrap += "\t" + "if (z_syscall_trap()) {\n"
251
252    valist_args = []
253    for argnum, (argtype, argname) in enumerate(args):
254        split = need_split(argtype)
255        decl = union_decl(argtype, split)
256        wrap += f"\t\t{decl} parm{argnum}"
257        if argtype != "va_list":
258            wrap += f" = {{ .val = {argname} }};\n"
259        else:
260            # va_list objects are ... peculiar.
261            wrap += ";\n" + "\t\t" + f"va_copy(parm{argnum}.val, {argname});\n"
262            valist_args.append(f"parm{argnum}.val")
263        if split:
264            mrsh_args.append(f"parm{argnum}.split.lo")
265            mrsh_args.append(f"parm{argnum}.split.hi")
266        else:
267            mrsh_args.append(f"parm{argnum}.x")
268
269    if ret64:
270        mrsh_args.append("(uintptr_t)&ret64")
271
272    if len(mrsh_args) > 6:
273        wrap += "\t\t" + "uintptr_t more[] = {\n"
274        wrap += "\t\t\t" + (",\n\t\t\t".join(mrsh_args[5:])) + "\n"
275        wrap += "\t\t" + "};\n"
276        mrsh_args[5:] = ["(uintptr_t) &more"]
277
278    invoke_args = ", ".join(mrsh_args + [syscall_id])
279    invoke = f"arch_syscall_invoke{len(mrsh_args)}({invoke_args})"
280
281    if ret64:
282        invoke = "\t\t" + f"(void) {invoke};\n"
283        retcode = "\t\t" + f"return ({func_type}) ret64;\n"
284    elif func_type == "void":
285        invoke = "\t\t" + f"(void) {invoke};\n"
286        retcode = "\t\t" + "return;\n"
287    elif valist_args:
288        invoke = "\t\t" + f"{func_type} invoke__retval = {invoke};\n"
289        retcode = "\t\t" + "return invoke__retval;\n"
290    else:
291        invoke = "\t\t" + f"return ({func_type}) {invoke};\n"
292        retcode = ""
293
294    wrap += invoke
295    for argname in valist_args:
296        wrap += "\t\t" + f"va_end({argname});\n"
297    wrap += retcode
298    if not userspace_only:
299        wrap += "\t" + "}\n"
300        wrap += "#endif\n"
301
302        # Otherwise fall through to direct invocation of the impl func.
303        # Note the compiler barrier: that is required to prevent code from
304        # the impl call from being hoisted above the check for user
305        # context.
306        impl_arglist = ", ".join([argrec[1] for argrec in args])
307        impl_call = f"z_impl_{func_name}({impl_arglist})"
308        wrap += "\t" + "compiler_barrier();\n"
309        ret = "return " if func_type != "void" else ""
310        wrap += "\t" + f"{ret}{impl_call};\n"
311
312    wrap += "}\n"
313
314    if fn not in notracing:
315        argnames = ", ".join([f"{argname}" for _, argname in args])
316        trace_argnames = ""
317        if len(args) > 0:
318            trace_argnames = ", " + argnames
319        trace_diagnostic = ""
320        if os.getenv('TRACE_DIAGNOSTICS'):
321            trace_diagnostic = f"#warning Tracing {func_name}"
322        if func_type != "void":
323            wrap += syscall_tracer_with_return_template.format(
324                func_type=func_type,
325                func_name=func_name,
326                argnames=argnames,
327                trace_argnames=trace_argnames,
328                syscall_id=syscall_id,
329                trace_diagnostic=trace_diagnostic,
330            )
331        else:
332            wrap += syscall_tracer_void_template.format(
333                func_type=func_type,
334                func_name=func_name,
335                argnames=argnames,
336                trace_argnames=trace_argnames,
337                syscall_id=syscall_id,
338                trace_diagnostic=trace_diagnostic,
339            )
340
341    return wrap
342
343
344# Returns an expression for the specified (zero-indexed!) marshalled
345# parameter to a syscall, with handling for a final "more" parameter.
346def mrsh_rval(mrsh_num, total):
347    if mrsh_num < 5 or total <= 6:
348        return f"arg{mrsh_num}"
349    else:
350        return f"(((uintptr_t *)more)[{mrsh_num - 5}])"
351
352
353def marshall_defs(func_name, func_type, args):
354    mrsh_name = "z_mrsh_" + func_name
355
356    nmrsh = 0  # number of marshalled uintptr_t parameter
357    vrfy_parms = []  # list of (argtype, bool_is_split)
358    for argtype, _ in args:
359        split = need_split(argtype)
360        vrfy_parms.append((argtype, split))
361        nmrsh += 2 if split else 1
362
363    # Final argument for a 64 bit return value?
364    if need_split(func_type):
365        nmrsh += 1
366
367    decl_arglist = ", ".join([" ".join(argrec) for argrec in args])
368    mrsh = f"extern {func_type} z_vrfy_{func_name}({decl_arglist});\n"
369
370    mrsh += f"uintptr_t {mrsh_name}(uintptr_t arg0, uintptr_t arg1, uintptr_t arg2,\n"
371    if nmrsh <= 6:
372        mrsh += "\t\t" + "uintptr_t arg3, uintptr_t arg4, uintptr_t arg5, void *ssf)\n"
373    else:
374        mrsh += "\t\t" + "uintptr_t arg3, uintptr_t arg4, void *more, void *ssf)\n"
375    mrsh += "{\n"
376    mrsh += "\t" + "_current->syscall_frame = ssf;\n"
377
378    for unused_arg in range(nmrsh, 6):
379        mrsh += f"\t(void) arg{unused_arg};\t/* unused */\n"
380
381    if nmrsh > 6:
382        mrsh += (
383            "\tK_OOPS(K_SYSCALL_MEMORY_READ(more, " + str(nmrsh - 5) + " * sizeof(uintptr_t)));\n"
384        )
385
386    argnum = 0
387    for i, (argtype, split) in enumerate(vrfy_parms):
388        decl = union_decl(argtype, split)
389        mrsh += f"\t{decl} parm{i};\n"
390        rval = mrsh_rval(argnum, nmrsh)
391        if split:
392            mrsh += "\t" + f"parm{i}.split.lo = {rval};\n"
393            argnum += 1
394            rval = mrsh_rval(argnum, nmrsh)
395            mrsh += "\t" + f"parm{i}.split.hi = {rval};\n"
396        else:
397            mrsh += "\t" + f"parm{i}.x = {rval};\n"
398        argnum += 1
399
400    # Finally, invoke the verify function
401    out_args = ", ".join([f"parm{i}.val" for i in range(len(args))])
402    vrfy_call = f"z_vrfy_{func_name}({out_args})"
403
404    if func_type == "void":
405        mrsh += "\t" + f"{vrfy_call};\n"
406        mrsh += "\t" + "_current->syscall_frame = NULL;\n"
407        mrsh += "\t" + "return 0;\n"
408    else:
409        mrsh += "\t" + f"{func_type} ret = {vrfy_call};\n"
410
411        if need_split(func_type):
412            rval = mrsh_rval(nmrsh - 1, nmrsh)
413            ptr = f"((uint64_t *){rval})"
414            mrsh += "\t" + f"K_OOPS(K_SYSCALL_MEMORY_WRITE({ptr}, 8));\n"
415            mrsh += "\t" + f"*{ptr} = ret;\n"
416            mrsh += "\t" + "_current->syscall_frame = NULL;\n"
417            mrsh += "\t" + "return 0;\n"
418        else:
419            mrsh += "\t" + "_current->syscall_frame = NULL;\n"
420            mrsh += "\t" + "return (uintptr_t) ret;\n"
421
422    mrsh += "}\n"
423
424    return mrsh, mrsh_name
425
426
427def analyze_fn(match_group, fn, userspace_only):
428    func, args = match_group
429
430    try:
431        if args == "void":
432            args = []
433        else:
434            args = [typename_split(a) for a in args.split(",")]
435
436        func_type, func_name = typename_split(func)
437    except SyscallParseException:
438        sys.stderr.write(f"In declaration of {func}\n")
439        raise
440
441    sys_id = "K_SYSCALL_" + func_name.upper()
442
443    marshaller = None
444    marshaller, handler = marshall_defs(func_name, func_type, args)
445    invocation = wrapper_defs(func_name, func_type, args, fn, userspace_only)
446
447    # Entry in _k_syscall_table
448    table_entry = f"[{sys_id}] = {handler}"
449
450    return (handler, invocation, marshaller, sys_id, table_entry)
451
452
453def parse_args():
454    global args
455    parser = argparse.ArgumentParser(
456        description=__doc__,
457        formatter_class=argparse.RawDescriptionHelpFormatter,
458        allow_abbrev=False,
459    )
460
461    parser.add_argument(
462        "-i", "--json-file", required=True, help="Read syscall information from json file"
463    )
464    parser.add_argument(
465        "-d", "--syscall-dispatch", required=True, help="output C system call dispatch table file"
466    )
467    parser.add_argument(
468        "-l", "--syscall-list", required=True, help="output C system call list header"
469    )
470    parser.add_argument(
471        "-o", "--base-output", required=True, help="Base output directory for syscall macro headers"
472    )
473    parser.add_argument(
474        "-s",
475        "--split-type",
476        action="append",
477        help="A long type that must be split/marshalled on 32-bit systems",
478    )
479    parser.add_argument(
480        "-x",
481        "--long-registers",
482        action="store_true",
483        help="Indicates we are on system with 64-bit registers",
484    )
485    parser.add_argument(
486        "--gen-mrsh-files", action="store_true", help="Generate marshalling files (*_mrsh.c)"
487    )
488    parser.add_argument(
489        "-e", "--syscall-exports-llext", help="output C system call export for extensions"
490    )
491    parser.add_argument(
492        "-w", "--syscall-weakdefs-llext", help="output C system call weak definitions"
493    )
494    parser.add_argument(
495        "-u",
496        "--userspace-only",
497        action="store_true",
498        help="Only generate the userpace path of wrappers",
499    )
500    args = parser.parse_args()
501
502
503def main():
504    parse_args()
505
506    if args.split_type is not None:
507        for t in args.split_type:
508            types64.append(t)
509
510    with open(args.json_file) as fd:
511        syscalls = json.load(fd)
512
513    invocations = {}
514    mrsh_defs = {}
515    mrsh_includes = {}
516    ids_emit = []
517    ids_not_emit = []
518    table_entries = []
519    handlers = []
520    emit_list = []
521    exported = []
522
523    for match_group, fn, to_emit in syscalls:
524        handler, inv, mrsh, sys_id, entry = analyze_fn(match_group, fn, args.userspace_only)
525
526        if fn not in invocations:
527            invocations[fn] = []
528
529        invocations[fn].append(inv)
530        handlers.append(handler)
531
532        if to_emit:
533            ids_emit.append(sys_id)
534            table_entries.append(entry)
535            emit_list.append(handler)
536            exported.append(handler.replace("z_mrsh_", "z_impl_"))
537        else:
538            ids_not_emit.append(sys_id)
539
540        if mrsh and to_emit:
541            syscall = typename_split(match_group[0])[1]
542            mrsh_defs[syscall] = mrsh
543            mrsh_includes[syscall] = f"#include <zephyr/syscalls/{fn}>"
544
545    with open(args.syscall_dispatch, "w") as fp:
546        table_entries.append("[K_SYSCALL_BAD] = handler_bad_syscall")
547
548        weak_defines = "".join(
549            [weak_template % name for name in handlers if name not in noweak and name in emit_list]
550        )
551
552        # The "noweak" ones just get a regular declaration
553        noweak_args = ", ".join([f"uintptr_t arg{i + 1}" for i in range(6)]) + ", void *ssf"
554        weak_defines += "\n".join([f"extern uintptr_t {s}({noweak_args});" for s in noweak])
555
556        fp.write(table_template % (weak_defines, ",\n\t".join(table_entries)))
557
558    exported.sort()
559
560    if args.syscall_weakdefs_llext:
561        with open(args.syscall_weakdefs_llext, "w") as fp:
562            # Provide weak definitions for all emitted syscalls
563            weak_refs = "\n".join(
564                f"extern __weak ALIAS_OF(no_syscall_impl) void * const {e};" for e in exported
565            )
566            fp.write(llext_weakdefs_template % weak_refs)
567
568    if args.syscall_exports_llext:
569        with open(args.syscall_exports_llext, "w") as fp:
570            # Export symbols for emitted syscalls
571            extern_refs = "\n".join(f"extern void * const {e};" for e in exported)
572            exported_symbols = "\n".join(f"EXPORT_GROUP_SYMBOL(SYSCALL, {e});" for e in exported)
573            fp.write(llext_exports_template % (extern_refs, exported_symbols))
574
575    # Listing header emitted to stdout
576    ids_emit.sort()
577    ids_emit.extend(["K_SYSCALL_BAD", "K_SYSCALL_LIMIT"])
578
579    ids_as_defines = ""
580    for i, item in enumerate(ids_emit):
581        ids_as_defines += f"#define {item} {i}\n"
582
583    if ids_not_emit:
584        # There are syscalls that are not used in the image but
585        # their IDs are used in the generated stubs. So need to
586        # make them usable but outside the syscall ID range.
587        ids_as_defines += "\n\n/* Following syscalls are not used in image */\n"
588        ids_not_emit.sort()
589        num_emitted_ids = len(ids_emit)
590        for i, item in enumerate(ids_not_emit):
591            ids_as_defines += f"#define {item} {i + num_emitted_ids}\n"
592
593    with open(args.syscall_list, "w") as fp:
594        fp.write(list_template % ids_as_defines)
595
596    os.makedirs(args.base_output, exist_ok=True)
597    for fn, invo_list in invocations.items():
598        out_fn = os.path.join(args.base_output, fn)
599
600        ig = re.sub("[^a-zA-Z0-9]", "_", "Z_INCLUDE_SYSCALLS_" + fn).upper()
601        include_guard = f"#ifndef {ig}\n#define {ig}\n"
602        tracing_include = ""
603        if fn not in notracing:
604            tracing_include = "#include <zephyr/tracing/tracing_syscall.h>"
605        header = syscall_template.format(
606            include_guard=include_guard,
607            tracing_include=tracing_include,
608            invocations="\n\n".join(invo_list),
609        )
610
611        with open(out_fn, "w") as fp:
612            fp.write(header)
613
614    # Likewise emit _mrsh.c files for syscall inclusion
615    if args.gen_mrsh_files:
616        for fn in mrsh_defs:
617            mrsh_fn = os.path.join(args.base_output, fn + "_mrsh.c")
618
619            with open(mrsh_fn, "w") as fp:
620                fp.write("/* auto-generated by gen_syscalls.py, don't edit */\n\n")
621                fp.write(mrsh_includes[fn] + "\n")
622                fp.write("\n")
623                fp.write(mrsh_defs[fn] + "\n")
624
625
626if __name__ == "__main__":
627    main()
628