Exec arguments and the initial user stack
Exec must turn pointers into the calling process into a self-contained startup image in a different address space. This chapter follows that transformation in xnu-12377.121.6: argument extraction, interpreter rewriting, kernel string storage, stack allocation, Apple-vector construction, and installation of ARM64 SP and PC. A worked example distinguishes kernel-buffer offsets from addresses in the new process.
Start with the process guide and process transitions. Executable loading explains how Mach-O supplies the new map and entry point; exec identity explains credentials, inherited resources and publication gates. Those operations surround the stack construction here.
1. Extract from the caller before writing into the target
The ordinary exec entry stores the supplied filename, argp and envp addresses in image_params. It records the caller's address width in IMGPF_WAS_64BIT_ADDR, creates the replacement proc/task/thread, and gives image activation a VFS context associated with that replacement. The syscall still runs on the old executing thread. posix_spawn initializes equivalent argument fields but has different process-construction and error-return surroundings. Exec setup, spawn argument setup.
After choosing a compatible Mach-O image, exec_mach_imgact() sets the destination address/data-width flags. It calls exec_extract_strings() before changing its local thread and task variables to the replacement and before loading/installing the new map. This order preserves access to the old argument pointers while collecting their contents. Destination width, extraction and target selection.
Three different address representations participate:
| Representation | Example holder | Meaning |
|---|---|---|
| Caller virtual address | ip_user_argv, an element read from that array |
An address to read with the caller's pointer width and copyin machinery |
| Kernel-buffer pointer or offset | ip_strings, ip_startargv, cur_string - ip_strings |
Storage owned by this activation; it is not a user-visible pointer |
| Target virtual address | string_area, reconstructed argv[i], saved SP |
An address in the replacement task's map |
The later copyout stage explicitly switches the executing thread's map context with vm_map_switch_to(get_task_map(task)), then restores it after copying or before leaving a copyout failure path. Assigning the replacement task to a local variable did not perform that address-space switch. Switch and string copy, normal restoration.
Mermaid source
flowchart TD
U["Caller: path, argv pointers, envp pointers"] -->|"copy path, pointers and strings"| K["Activation-owned kernel buffer"]
I["Optional interpreter line"] -->|"rewrite argument sequence"| K
L["Mach-O load result"] -->|"allocate stack and choose entry"| T["Replacement task and thread"]
T -->|"supply stack metadata and thread port name"| A["Append Apple strings"]
K --> A
A -->|"switch map and copy bytes"| S["Target string area"]
A -->|"translate offsets and copy pointer arrays"| P["Target argv, envp and Apple vectors"]
S --> R["Install argc SP, then optional dyld header slot"]
P --> R
R --> G["Remaining exec publication and return gates"]
This is the successful construction route. Every copy and allocation still has an error path; no arrow means that the replacement is already allowed to execute.
2. One cached allocation carries strings, scratch space and image bytes
image_params holds counts (ip_argc, ip_envc, ip_applec), positions within a kernel string buffer, and two remaining-space counters. These fields describe a packed representation, not arrays of kernel pointers corresponding to the final user vectors. Field definitions.
exec_activate_image() obtains the argument buffer before resolving the saved pathname. execargs_alloc() admits a limited number of simultaneous users under execargs_cache_lock. When no slot is available, it sleeps interruptibly and rechecks the count after waking. It removes a cached mapping under the lock, then drops that lock before allocating a new purgeable mapping or marking a reused one nonvolatile. An interrupted admission returns EINTR; a failed VM operation reaches the explicit ENOMEM branch, with assertions also present in this source. Activation entry, cache admission and acquisition, VM operations.
The allocation size is NCARGS + PAGE_SIZE + PAGE_SIZE: the extra capacity covers Apple strings and an executable-header page. The last page starts at ip_vdata = ip_strings + NCARGS + PAGE_SIZE. exec_copyout_strings() also uses the space after the assembled strings to construct the outgoing pointer arrays. Do not depict the whole allocation as already containing user-stack bytes. Allocation-size definition, initial fields, pointer scratch placement.
| Counter | Charged by | Deliberately excluded |
|---|---|---|
ip_argspace, initially NCARGS |
Argument/environment string bytes including NULs; each destination-width argument/environment pointer; their two terminating null pointers; padding after the combined string region | Executable-path prefix/string, Apple strings and their pointer vector |
ip_strspace, initially NCARGS + PAGE_SIZE |
Bytes appended to the packed string area, including its alignment padding | Pointer slots reserved through ip_argspace; these are constructed later |
NCARGS aliases ARG_MAX. The kernel-private definition in this checkout tests whether XNU_TARGET_OS_OSX is defined, selecting 1 MiB when it is and 256 KiB otherwise. A build's preprocessor definitions determine the branch; these numbers are not inferred from an ARM64 filename or a userspace header alone. Alias, conditional definitions.
exec_add_user_string() selects the relevant remaining budget, uses copyinstr() for user strings or copystr() for kernel strings, and advances the buffer/counters by the reported copied length. If the copy reports ENAMETOOLONG, the helper loops; a depleted selected budget returns E2BIG. Error handling must therefore treat the partially built buffer as consumed scratch state, rather than assume a failed append changed nothing. Complete append body.
3. A script changes both the lookup path and the argument sequence
The initial saved string is executable_path= followed by a copy of the invocation pathname. The prefix and pathname consume total string space but do not consume the argument budget. Before saving, exec_save_path() clears the prefix/path region because the allocation may have been reused. Path construction.
For a #! image, exec_shell_imgact() accepts one interpreter redirection: it declines an image already marked IMGPF_INTERPRET, and also declines a script inside a previously selected fat slice. The parser works within IMG_SHSIZE (512 bytes), recognizes space/tab as whitespace and #/newline as line termination, and returns ENOEXEC for an absent interpreter or missing bounded line end. It is not a shell quoting or variable-expansion parser. Character rules, bound, admission and parsing.
It resets the string-buffer counters, saves the interpreter pathname as the new executable_path, and saves the whole interpreter-plus-arguments text separately in ip_interp_buffer. Returning the internal value -3 makes the activation loop release the old vnode lookup state and resolve the interpreter. That internal result is not a userspace errno. Reset and interpreter storage, path replacement, lookup restart.
During later string extraction, the stored interpreter line is split on spaces/tabs. Its tokens become the first arguments. A non-null old argv[0] is skipped; the script pathname is inserted, followed by the remaining supplied arguments. If the selected set-ID script route created ip_interp_sugid_fd, the inserted string is /dev/fd/<number> instead. Descriptor construction is guarded by CONFIG_SETUID and the retained mode bits; the identity companion explains their policy. Tokenization and old-argument skip, script argument insertion, optional descriptor construction.
For an illustrative invocation with pathname /tmp/job, first line #! /bin/tool -x\n, and supplied argv = {"custom-name", "input", NULL}, the new argument strings are:
Illustrative rewritten argv:
[0] /bin/tool
[1] -x
[2] /tmp/job
[3] input
[4] NULL
First Apple string: executable_path=/bin/tool
The arbitrary old string custom-name does not become the script pathname. The pathname inserted during extraction is copied from ip_user_fname again on the ordinary script path; the earlier saved lookup pathname is not a single immutable snapshot of every later input. Similarly, pointer arrays and strings are read in separate copy operations. The implementation does not freeze all caller memory while collecting them.
4. Source pointer width and destination pointer width have separate jobs
The distinction appears at the start of extraction. Source: kern_exec.c, lines 6729–6735.
exec_extract_strings(struct image_params *imgp)
{
int error = 0;
int ptr_size = (imgp->ip_flags & IMGPF_WAS_64BIT_ADDR) ? 8 : 4;
int new_ptr_size = (imgp->ip_flags & IMGPF_IS_64BIT_ADDR) ? 8 : 4;
user_addr_t argv = imgp->ip_user_argv;
user_addr_t envv = imgp->ip_user_envv;
ptr_size controls copyinptr() and stepping over the caller's arrays. new_ptr_size controls pointer-budget reservations and alignment for the image being built. For the ARM64 example below both are eight, but the two flags must remain distinct when reading shared implementation code; their presence does not guarantee every source/destination architecture combination is accepted by image grading.
After aligning the saved path to the destination pointer width, extraction marks ip_startargv and walks argument pointers until a null element. A null array address skips that walk. Each copied string increments ip_argc only after its bytes and destination pointer slot have passed their budget checks. It reserves the terminating argv pointer, records ip_endargv, then performs the analogous environment loop. The environment receives no shell parsing in this helper. Argument walk, environment walk.
Finally the combined argument/environment string region is padded with zero bytes to the destination pointer width. This padding consumes both counters; the resulting ip_endenvv will be the start of appended Apple strings. The path's earlier padding consumed only ip_strspace. Individual strings inside a region are separated only by their terminating NUL and need not start at aligned addresses. Initial path padding, combined-region padding.
For argv = {"demo", "-v", NULL} and envp = {"MODE=1", NULL} in an eight-byte destination, the argument/environment strings take 5 + 3 + 7 = 15 bytes. Five pointer slots take 40 bytes: two arguments, one environment entry and two terminators. One padding byte brings the total NCARGS charge to 56 bytes. The pathname and Apple strings have separate accounting.
5. Stack allocation supplies a top, an extent and a no-access region
For the selected LC_MAIN route, the loader obtains the architecture's default stack location, applies its slide and page truncation, and records allocation requirements. An explicit nonzero stacksize becomes the usable size; its allocation includes an extra PAGE_SIZE, with overflow checks. With no explicit size, allocation uses MAXSSIZ and the later stack helper applies the process limit. LC_MAIN also requests dyld; the kernel does not use its entryoff as the initial PC. Loader decisions, ARM64 default selection.
create_unix_stack() first records the requested top in p->user_stack, then allocates if user_stack_alloc_size > 0. It tries a fixed mapping below that top; if allocation fails, it tries anywhere and replaces the recorded top with addr + size. A custom_stack flag does not universally mean that this helper allocates nothing: an explicit LC_MAIN.stacksize sets that flag and still supplies a nonzero allocation size. Stack setup and fallback.
The helper sets the low-address portion of the allocation to VM_PROT_NONE. For an explicit usable size, that protected amount is PAGE_SIZE; for a default stack it derives the protected amount from allocation size minus RLIMIT_STACK, using the target map's page mask. If protection fails, it deallocates the new allocation before returning. Kernel PAGE_SIZE, map page size, requested usable size and rounded allocation size are separate quantities on mixed-page-size paths. Guard and limit enforcement.
The worked example selects matching 16 KiB kernel and target-map pages, a 128 KiB explicit usable stack (0x20000) and a 16 KiB low guard. Its allocation size is 0x24000. All numerical addresses below are EL0 CPU virtual addresses in the target map, not physical addresses or kernel-buffer addresses. Fixed allocation succeeds at the hypothetical location; no claim is made about a device's actual ASLR choice.
6. The Apple vector contains metadata and a published port name
exec_add_apple_strings() sets ip_applec = 1 because the first Apple string, the saved executable path, is already at the buffer's beginning. It appends the remaining strings after ip_endenvv and finally pads that tail to the destination pointer width. exec_copyout_strings() will join those two physically separated parts into one logical pointer vector. Initial count and PFZ entry, tail padding.
| Entry or group | Local construction rule |
|---|---|
executable_path, pfz |
Saved path plus the commpage-text location selected by destination pointer width |
stack_guard, malloc_entropy, ptr_munge |
Fresh random values formatted as hexadecimal strings; the stack-guard helper clears one byte of the numeric value before formatting |
main_stack |
For unixproc, four values: top, usable size, allocation base and allocation size |
executable_file, dyld_file |
File identity from present vnode attributes and nonzero dyld identity fields respectively |
executable_cdhash, executable_boothash |
Added together when ubc_cs_getcdhash() succeeds; the latter derives from boot salt, boot UUID and the executable cdhash |
th_port |
The name returned by attempting main-thread send-right copyout into the new task's IPC space when ip_new_thread exists |
security_config |
Added even when the task's security configuration value is zero |
| Conditional additions | Spawn nano-allocator selection, development dyld flags, subsystem root, pointer-authentication compatibility, macOS mixed-page-size selection, malloc experiment factors, SEC/MTE and hardened-browser metadata |
Entropy formatting, entropy/stack entries, file/digest entries, optional entries, remaining conditional/configuration entries.
The stack_guard string itself is normal NUL-terminated text. The helper's embedNUL operation clears bits 8–15 of the random number, then formats the number using %llx; it does not insert a NUL into the middle of the hexadecimal text. Keep this distinction when interpreting the nearby comment about an embedded NUL. Numeric mask and formatting.
The thread-port entry can publish a right before any stack copyout. Source: kern_exec.c, lines 7262–7274.
if ((new_thread = imgp->ip_new_thread) != THREAD_NULL) {
thread_reference(new_thread);
sright = convert_thread_to_port_immovable(new_thread);
task_t new_task = get_threadtask(new_thread);
mach_port_name_t name = ipc_port_copyout_send_pinned(sright, get_task_ipcspace(new_task));
char port_name_hex_str[strlen(MAIN_TH_PORT_KEY) + HEX_STR_LEN32 + 1];
snprintf(port_name_hex_str, sizeof(port_name_hex_str), MAIN_TH_PORT_KEY "0x%x", name);
error = exec_add_user_string(imgp, CAST_USER_ADDR_T(port_name_hex_str), UIO_SYSSPACE, FALSE);
if (error) {
goto bad;
}
imgp->ip_applec++;
The formatted value is a task-local Mach port name, not a thread_t pointer. The code references the new thread, converts that reference to an immovable send right, and attempts to copy the right into the new task's IPC space with the pinned-send helper. On success, a subsequent string-append failure does not undo that publication in this block; broader activation/task cleanup is responsible for abandoning the failed image. The source comment describes an intended libpthread consumer, but this chapter does not reconstruct that external library. Ownership comment, conversion body.
The copyout helper returns MACH_PORT_DEAD for KERN_INVALID_CAPABILITY, or MACH_PORT_NULL for other copyout failures. Exec formats the returned name without checking these sentinel values. A th_port string alone therefore does not prove that a usable send right was installed. The numerical example selects successful copyout. Copyout result conversion and pinned wrapper.
7. Copyout rebuilds pointers from offsets
Let T be p->user_stack on entry, S the fully padded string-byte count, and w the destination pointer width. With a = ip_argc, e = ip_envc, and k = ip_applec, the helper lays out:
Illustrative formulas matching exec_copyout_strings:
string_area = T - S
pointer_bytes = (a + e + k + 3) * w
ptr_area = string_area - pointer_bytes
argc_area = ptr_area - w
The three extra pointer slots terminate argv, envp and the Apple vector. The argc occupies one pointer-sized slot even though its logical count is an integer. Size and placement.
The copy sequence is: publish p_argc/p_argslen and the helper's returned stack address, copy all string bytes, construct/copy the pointer arrays, then copy argc. p_argslen is S, the string-area size, not the size of every slot below it. The process fields are protected by proc_lock; this is not a lock held across the user-memory copies. Metadata and first copy, pointer/argc copies.
Each output pointer is derived from a kernel-buffer offset, added to the target string base. Source: kern_exec.c, lines 6655–6666.
for (j = 0; j < descriptors[i].count; j++) {
user_addr_t cur_address = string_area + (cur_string - imgp->ip_strings);
/* Copy out the pointer to the current string. Alignment has been verified */
if (ptr_size == 8) {
*(uint64_t *)ptr_buffer = (uint64_t)cur_address;
} else {
*(uint32_t *)ptr_buffer = (uint32_t)cur_address;
}
ptr_buffer = (void *)((uintptr_t)ptr_buffer + ptr_size);
cur_string += strlen(cur_string) + 1; /* Only a NUL between strings in the same area */
The descriptor list emits argv, envp, the single path string, and then the other ip_applec - 1 strings. The path descriptor has no null terminator; the final descriptor supplies the one Apple-vector terminator. This reconnects ip_strings and ip_endenvv without copying their text into a second Apple-only string block. Descriptor list.
Mermaid source
flowchart TD
K["Kernel buffer: path, argv/env strings, remaining Apple strings"] -->|"copy S bytes"| B["Target string base B = T minus S"]
O["Offset: cur_string minus ip_strings"] -->|"add B"| U["Target virtual pointer B plus offset"]
B -->|"base for translation"| U
U -->|"store at destination width"| V["argv / envp / Apple pointer slots"]
V -->|"terminate each vector"| Z["Three null pointer slots"]
There is no requirement that each pointed-to string be pointer-aligned. The pointer slots themselves are aligned to w; packed string starts can be at arbitrary byte offsets.
8. Worked ARM64 stack layout
Select an ordinary dynamically linked 64-bit-address image, eight-byte source/destination pointers, and the stack allocation from section 5. Let its successful allocation be [0x167fdc000, 0x168000000): the low [0x167fdc000, 0x167fe0000) is the guard, and the remaining 0x20000 bytes are usable. Set T = 0x168000000 and the main image's Mach header address to the hypothetical H = 0x100000000.
The invocation is /tmp/demo, argv = {"demo", "-v", NULL}, envp = {"MODE=1", NULL}. Select exactly twelve Apple entries: path, PFZ, three entropy entries, stack description, two file identities, two digest entries, main-thread port and security configuration. The optional additions listed above are absent in this example. The file IDs, port name, entropy and digest characters below are hypothetical values for layout arithmetic; the two 40-character digest strings are placeholders, not a calculated hash pair or recorded launch.
The table specifies every string. Each byte count includes its final NUL. The two digest rows use 00 × 20 and 11 × 20 to mean exactly forty hexadecimal characters following the key; that notation is not literal buffer content.
| Logical element | Buffer offset | Bytes | Illustrative string |
|---|---|---|---|
apple[0] |
0x000 |
26 | executable_path=/tmp/demo |
| Path padding | 0x01a |
6 | Zero bytes |
argv[0] |
0x020 |
5 | demo |
argv[1] |
0x025 |
3 | -v |
envp[0] |
0x028 |
7 | MODE=1 |
| Argument/environment padding | 0x02f |
1 | Zero byte |
apple[1] |
0x030 |
16 | pfz=0x1ffff0000 |
apple[2] |
0x040 |
23 | stack_guard=0x12340078 |
apple[3] |
0x057 |
23 | malloc_entropy=0x1,0x2 |
apple[4] |
0x06e |
14 | ptr_munge=0x3 |
apple[5] |
0x07c |
51 | main_stack=0x168000000,0x20000,0x167fdc000,0x24000 |
apple[6] |
0x0af |
24 | executable_file=0x1,0x2 |
apple[7] |
0x0c7 |
18 | dyld_file=0x1,0x3 |
apple[8] |
0x0d9 |
59 | executable_cdhash= + 00 × 20 |
apple[9] |
0x114 |
61 | executable_boothash= + 11 × 20 |
apple[10] |
0x151 |
14 | th_port=0x103 |
apple[11] |
0x15f |
20 | security_config=0x0 |
| Apple-tail padding | 0x173 |
5 | Zero bytes |
Thus S = 0x178 = 376 bytes, pointer_bytes = (2 + 1 + 12 + 3) × 8 = 0x90, and the target string base is B = 0x167fffe88. The resulting slots, in increasing address order, are:
| Target virtual address | Contents |
|---|---|
0x167fffde8 |
H = 0x100000000, the extra dyld header slot installed after the string helper |
0x167fffdf0 |
argc = 2, in an eight-byte slot |
0x167fffdf8 |
argv[0] = B + 0x20 = 0x167fffea8 |
0x167fffe00 |
argv[1] = B + 0x25 = 0x167fffead |
0x167fffe08 |
Null argv terminator |
0x167fffe10 |
envp[0] = B + 0x28 = 0x167fffeb0 |
0x167fffe18 |
Null environment terminator |
0x167fffe20 |
apple[0] = B |
0x167fffe28 through 0x167fffe78 |
Eleven Apple pointers, using the offsets for apple[1] through apple[11] above |
0x167fffe80 |
Null Apple-vector terminator |
0x167fffe88 through 0x167ffffff |
The 376 string/padding bytes; the exclusive end is T |
p->p_argc becomes 2 and p->p_argslen becomes 376. p->user_stack remains the high address T; the saved machine SP will point below the strings and vectors. These two addresses have different roles. Changing any optional Apple entry can change both S and the pointer count, so a fixed offset to argv must be derived from the selected layout rather than copied from another launch.
9. Saved SP and PC are installed separately
After exec_copyout_strings() succeeds, the activator calls thread_setuserstack(thread, ap) with ap = argc_area. On ARM64 that helper writes the supplied value directly through set_saved_state_sp(). For a loaded dynamic linker, the activator then moves SP down by one destination-width slot and copies load_result.mach_header there. SP setter, dyld prefix operation.
The actual adjustment performs no additional rounding. Source: status.c, lines 2455–2461.
sv = get_user_regs(thread);
sp = get_saved_state_sp(sv);
sp += adjust;
set_saved_state_sp(sv, sp);
return sp;
For the example, the string helper returns 0x167fffdf0; the dynamic-image SP becomes 0x167fffde8. That value is eight-byte aligned but not sixteen-byte aligned. The inspected construction aligns the string subregions and pointer slots to the destination pointer width; it does not perform an extra sixteen-byte SP adjustment here. This is a kernel-to-startup-code layout, not evidence for a general C-function calling convention or for how dyld later aligns its own calls.
PC was installed earlier by activate_exec_state(): initialize thread state, apply the loader's saved thread-state flavors, then call thread_setentrypoint(thread, result->entry_point). The ARM64 setter writes user saved-state PC, using the PAC-safe interrupt wrapper when selected. For the dynamic-image route, load_dylinker() supplies the dynamic linker's entry/state; the extra stack word remains the main image's header address, not that entry PC. Exec state activation, ARM64 PC setter, dynamic-linker result transfer.
The chapter stops at prepared saved state and memory. Actual userspace return still depends on the exec publication and return gates and the ARM64 entry/return machinery. External dyld and libSystem startup code decide how to consume the vectors and reach the application.
10. Failure preserves the stage's actual progress
Mermaid source
sequenceDiagram
participant E as Executing kernel thread
participant K as Kernel argument buffer
participant T as Target task and map
E->>K: Copy caller pointers and strings
E->>T: Install map, allocate stack, prepare PC
E->>T: Attempt main-thread send-right copyout
E->>K: Append its formatted name and other Apple strings
E->>T: Switch map context
E->>T: Set argument metadata and copy string bytes
E->>T: Copy pointer arrays and argc
alt All string copies succeed
E->>T: Set SP and copy dyld header slot
E->>E: Restore map context
else A string copy fails
E->>E: Restore map context
E->>T: Enter late activation failure handling
end
E->>K: Return argument allocation to purgeable cache
The diagram selects a replacement thread and dynamic Unix image; the success branch's dyld-header copy can itself fail and take late failure handling. It is not a transaction diagram with rollback of every earlier change.
| Stage and error | State that can already exist |
|---|---|
| Argument pointer/string copy fault or budget failure | Kernel-buffer bytes/counters from earlier arguments; caller arrays are not rewritten |
| Stack allocation or guard failure | Recorded stack metadata; the helper deallocates a newly allocated stack if its protection operation fails |
| Apple append failure | Prior metadata and the main-thread send right if its copyout already occurred |
| String-area copyout failure | Updated p_argc, p_argslen and local returned ap; a copy fault does not establish that no destination bytes were written |
| Pointer-vector or argc copyout failure | The successful string-area copy, and possibly earlier pointer/count bytes; saved SP is not installed by the caller until the whole helper succeeds |
| Dyld-header copyout failure | A complete argument image and already adjusted saved SP |
String and dyld-prefix copy errors restore the caller's map context before branching to badtoolate. In non-spawn handling that path sends a fatal reason, terminates the exec-copy task when applicable, and sets the internal error to zero because the old image cannot simply resume. Ordinary spawn keeps its error for child cleanup. A zero return from that internal path therefore does not establish a runnable new program. String-copy failure, dyld-prefix failure, late-failure body.
exec_activate_image() returns its temporary buffer through execargs_free() on its common exit. The free helper marks the mapping volatile, restores it to the cache and clears imgp->ip_strings under the cache lock, then wakes admission waiters after unlocking. This ends the activation's ownership; it is not an immediate VM deallocation or a claim that every byte was zeroed. Subsequent outer exec/spawn cleanup checks the pointer before attempting another free. Activation exit, cache return, outer exec cleanup, outer spawn cleanup.
The obsolete map has a separate owner: swap_task_map() transfers the new map to the target and places the old map in ip_free_map. The outer entry releases that map after dropping vnode iocounts. Buffer reuse, user-stack lifetime and task/map destruction therefore end at different points. Map transfer, ordered release.
11. Evidence and limits
The local sysctl_procargsx_cow test constructs a large argument vector, asserts successful spawn, then asserts successful size-query/data retrieval and a reported area larger than 64 KiB. It gives evidence for the test's large-argument scenario and adjacent retrieval path. It does not inspect startup SP, every pointer slot, script rewriting, or the optional Apple entries shown here. Argument construction, assertions. No kernel or userspace test was executed for this chapter.
This review follows common exec/spawn argument collection and cleanup, the interpreter helper, selected LC_MAIN stack requirements, Apple construction, generic copyout and ARM64 saved-state setters. The numerical example selects equal 16 KiB page sizes and a dynamic 64-bit-address Unix image; it does not validate mixed-page-size configurations, Rosetta paths, every custom-stack image, or all platform admission rules. The format and local creation of Apple strings are visible; the external consumers' behavior and private policy decisions are not reconstructed. For Mach-O parsing, identity transitions and return assembly, use the companion chapters rather than infer those mechanisms from a finished stack image.