Skip to main content

taproot_c_scape/
va.rs

1//! A hand-rolled SysV AMD64 `va_list`.
2//!
3//! Stable Rust can declare and call C variadic functions but cannot define
4//! them, so every variadic libc entry point here splits in two: a
5//! `#[unsafe(naked)]` shim generated by [`vararg_entry!`], and an ordinary
6//! `extern "C"` implementation function. The shim performs the callee half
7//! of the variadic protocol from psABI 3.5.7: it spills the argument
8//! registers into a register save area, builds a [`VaListTag`], and calls
9//! the implementation with the named arguments plus the tag's address.
10//! [`VaListTag::arg`] is `va_arg`.
11//!
12//! The constants in this file are the psABI's, cross-checked against the
13//! prologue and `va_arg` lowering gcc 15.2 emits for a variadic callee:
14//! six 8-byte GP slots at save area offsets 0..48, eight 16-byte SSE slots
15//! at 48..176 (`movaps`, so the area must be 16-aligned), `gp_offset`
16//! starting at 8 times the named GP count, `fp_offset` starting at 48,
17//! register fetches while the offsets stay below 48 and 176, and an
18//! 8-byte overflow cursor that starts at the first caller stack slot
19//! (entry `rsp + 8`).
20
21use core::ffi::c_void;
22
23/// One SysV AMD64 `va_list` element: the four-field cursor that C
24/// compilers give `__builtin_va_list` (psABI figure 3.34).
25#[repr(C)]
26#[derive(Clone, Copy, Debug)]
27pub struct VaListTag {
28    /// Byte offset into `reg_save_area` of the next GP argument. Starts
29    /// at 8 times the number of named GP arguments; 48 means exhausted.
30    pub gp_offset: u32,
31    /// Byte offset into `reg_save_area` of the next SSE argument. Starts
32    /// at 48 plus 16 per named SSE argument (our entries have none, so
33    /// always 48); 176 means exhausted.
34    pub fp_offset: u32,
35    /// The next stack-passed argument slot.
36    pub overflow_arg_area: *mut c_void,
37    /// The 176-byte register spill area the entry shim built.
38    pub reg_save_area: *mut c_void,
39}
40
41/// Where the GP slots in the register save area end: 6 slots of 8 bytes.
42const GP_AREA_END: u32 = 48;
43/// Where the SSE slots end: 48 plus 8 slots of 16 bytes.
44const SSE_AREA_END: u32 = 176;
45
46impl VaListTag {
47    /// `va_arg`: fetch the next variadic argument as a `T`.
48    ///
49    /// # Safety
50    ///
51    /// `T` must be the type the caller passed in this position, after C's
52    /// default argument promotions, and every earlier variadic argument
53    /// must already have been fetched with its own matching type.
54    #[inline]
55    pub unsafe fn arg<T: VaArg>(&mut self) -> T {
56        // SAFETY: the caller upholds `VaArg::va_arg`'s identical contract.
57        unsafe { T::va_arg(self) }
58    }
59
60    /// The psABI 3.5.7 fetch sequence for one INTEGER-class argument:
61    /// take the register slot at `gp_offset` while any remain, otherwise
62    /// fall through to the overflow area.
63    #[inline]
64    unsafe fn next_gp_slot(&mut self) -> *mut c_void {
65        if self.gp_offset < GP_AREA_END {
66            // SAFETY: `gp_offset` below 48 indexes one of the six GP
67            // slots the entry shim spilled.
68            let slot = unsafe { self.reg_save_area.byte_add(self.gp_offset as usize) };
69            self.gp_offset += 8;
70            slot
71        } else {
72            // SAFETY: register slots exhausted, so per `arg`'s contract
73            // the caller passed this argument on the stack.
74            unsafe { self.next_overflow_slot() }
75        }
76    }
77
78    /// The same sequence for one SSE-class argument: 16-byte register
79    /// slots from 48 up, then the overflow area.
80    #[inline]
81    unsafe fn next_sse_slot(&mut self) -> *mut c_void {
82        if self.fp_offset < SSE_AREA_END {
83            // SAFETY: `fp_offset` in 48..176 indexes one of the eight SSE
84            // slots the entry shim spilled.
85            let slot = unsafe { self.reg_save_area.byte_add(self.fp_offset as usize) };
86            self.fp_offset += 16;
87            slot
88        } else {
89            // SAFETY: as in `next_gp_slot`.
90            unsafe { self.next_overflow_slot() }
91        }
92    }
93
94    /// Take one 8-byte slot off the caller's stack area. Everything this
95    /// walker fetches is 8 bytes with at most 8-byte alignment, and the
96    /// area starts 8-aligned (entry `rsp + 8`), so the cursor never needs
97    /// the psABI's realignment step for 16-aligned types.
98    #[inline]
99    unsafe fn next_overflow_slot(&mut self) -> *mut c_void {
100        let slot = self.overflow_arg_area;
101        // SAFETY: the caller passed an argument in this slot, so the
102        // address one slot past it is still within (or one past) its
103        // argument area.
104        self.overflow_arg_area = unsafe { slot.byte_add(8) };
105        slot
106    }
107}
108
109/// Types [`VaListTag::arg`] can fetch.
110///
111/// C's default argument promotions mean narrower types never reach a
112/// variadic callee: `char`, `short` and `_Bool` arrive as `int`, and
113/// `float` arrives as `double`. That is why there is no `f32` impl and
114/// none below 32 bits: fetch the promoted type and narrow afterwards,
115/// exactly as C code must. `c_int`, `c_long` and friends are aliases of
116/// the primitives below on every x86_64 Linux target.
117pub trait VaArg: Copy {
118    /// Fetch one argument of this type off the walker.
119    ///
120    /// # Safety
121    ///
122    /// Same contract as [`VaListTag::arg`].
123    unsafe fn va_arg(tag: &mut VaListTag) -> Self;
124}
125
126/// INTEGER-class one-slot types.
127macro_rules! gp_va_arg {
128    ($($int:ty),*) => {$(
129        impl VaArg for $int {
130            #[inline]
131            unsafe fn va_arg(tag: &mut VaListTag) -> Self {
132                // SAFETY: `arg`'s contract puts a value of this type in
133                // the next GP slot; a 32-bit value occupies the slot's
134                // low bytes on this little-endian target, which is where
135                // a plain read of `Self` looks. Slots are 8-aligned.
136                unsafe { tag.next_gp_slot().cast::<Self>().read() }
137            }
138        }
139    )*};
140}
141
142gp_va_arg!(i32, u32, i64, u64, isize, usize);
143
144impl<T> VaArg for *const T {
145    #[inline]
146    unsafe fn va_arg(tag: &mut VaListTag) -> Self {
147        // SAFETY: pointers are INTEGER class, and a thin `*const T` is
148        // exactly one 8-aligned slot.
149        unsafe { tag.next_gp_slot().cast::<Self>().read() }
150    }
151}
152
153impl<T> VaArg for *mut T {
154    #[inline]
155    unsafe fn va_arg(tag: &mut VaListTag) -> Self {
156        // SAFETY: as for `*const T`.
157        unsafe { tag.next_gp_slot().cast::<Self>().read() }
158    }
159}
160
161impl VaArg for f64 {
162    #[inline]
163    unsafe fn va_arg(tag: &mut VaListTag) -> Self {
164        // SAFETY: doubles are SSE class; the value is the low 8 bytes of
165        // a 16-byte register slot or a whole 8-byte overflow slot, both
166        // at least 8-aligned.
167        unsafe { tag.next_sse_slot().cast::<f64>().read() }
168    }
169}
170
171/// Generates the `#[unsafe(naked)]` C entry point for a variadic libc
172/// function: the entry performs the variadic callee protocol and then
173/// calls an ordinary implementation function that receives the named
174/// arguments plus a `*mut VaListTag`.
175///
176/// ```ignore
177/// vararg_entry! {
178///     #[no_mangle]
179///     unsafe extern "C" fn printf(fmt: *const c_char, ...) -> c_int => printf_impl
180/// }
181/// ```
182///
183/// One to four named arguments are supported, and every named argument
184/// must be INTEGER class (an integer or a thin pointer): that covers the
185/// whole printf/execl/syslog/`__chk` surface. The declared return type
186/// (or none) must be INTEGER class or `f64`; it rides back through
187/// rax/xmm0 untouched.
188///
189/// Exported (macro_export) so the taproot cdylib crate can generate its
190/// scanf-family entries with the same emitter.
191#[macro_export]
192macro_rules! vararg_entry {
193    ($(#[$attr:meta])* unsafe extern "C" fn $name:ident(
194        $a1:ident: $t1:ty, ...
195    ) $(-> $ret:ty)? => $imp:path) => {
196        $crate::__vararg_entry_emit! {
197            [$(#[$attr])*] $name, ($a1: $t1), ($($ret)?), $imp,
198            gp = 8, tag_reg = "rsi"
199        }
200    };
201    ($(#[$attr:meta])* unsafe extern "C" fn $name:ident(
202        $a1:ident: $t1:ty, $a2:ident: $t2:ty, ...
203    ) $(-> $ret:ty)? => $imp:path) => {
204        $crate::__vararg_entry_emit! {
205            [$(#[$attr])*] $name, ($a1: $t1, $a2: $t2), ($($ret)?), $imp,
206            gp = 16, tag_reg = "rdx"
207        }
208    };
209    ($(#[$attr:meta])* unsafe extern "C" fn $name:ident(
210        $a1:ident: $t1:ty, $a2:ident: $t2:ty, $a3:ident: $t3:ty, ...
211    ) $(-> $ret:ty)? => $imp:path) => {
212        $crate::__vararg_entry_emit! {
213            [$(#[$attr])*] $name, ($a1: $t1, $a2: $t2, $a3: $t3), ($($ret)?), $imp,
214            gp = 24, tag_reg = "rcx"
215        }
216    };
217    ($(#[$attr:meta])* unsafe extern "C" fn $name:ident(
218        $a1:ident: $t1:ty, $a2:ident: $t2:ty, $a3:ident: $t3:ty, $a4:ident: $t4:ty, ...
219    ) $(-> $ret:ty)? => $imp:path) => {
220        $crate::__vararg_entry_emit! {
221            [$(#[$attr])*] $name, ($a1: $t1, $a2: $t2, $a3: $t3, $a4: $t4), ($($ret)?), $imp,
222            gp = 32, tag_reg = "r8"
223        }
224    };
225}
226
227/// The emitter behind [`vararg_entry!`]: one naked entry, with the
228/// psABI-derived constants baked in. `gp` is `8 * named_gp_args` and
229/// `tag_reg` is the GP argument register after the last named one.
230#[macro_export]
231#[doc(hidden)]
232macro_rules! __vararg_entry_emit {
233    (
234        [$($attr:tt)*] $name:ident, ($($arg:ident: $ty:ty),*), ($($ret:ty)?), $imp:path,
235        gp = $gp:literal, tag_reg = $tag_reg:literal
236    ) => {
237        // The entry hands the implementation the named arguments plus the
238        // tag; hold the two signatures together at compile time.
239        const _: unsafe extern "C" fn($($ty,)* *mut $crate::va::VaListTag) $(-> $ret)? = $imp;
240
241        $($attr)*
242        #[unsafe(naked)]
243        unsafe extern "C" fn $name($($arg: $ty),*) $(-> $ret)? {
244            ::core::arch::naked_asm!(
245                // The callee half of the variadic protocol (psABI 3.5.7).
246                //
247                // On entry rsp is 8 mod 16 (a 16-aligned stack plus the
248                // return address, psABI 3.2.2), so this 200-byte frame
249                // puts rsp back on a 16-byte boundary: the movaps stores
250                // below need that, and so does the ABI at the call below.
251                //
252                //   [rsp +   0 .. 176)  register save area
253                //   [rsp + 176 .. 200)  the VaListTag
254                "sub rsp, 200",
255                // Spill the six GP argument registers into slots 0..48.
256                // The named slots below gp_offset are never read back;
257                // spilling them anyway keeps the shim uniform.
258                "mov [rsp], rdi",
259                "mov [rsp + 8], rsi",
260                "mov [rsp + 16], rdx",
261                "mov [rsp + 24], rcx",
262                "mov [rsp + 32], r8",
263                "mov [rsp + 40], r9",
264                // AL carries the caller's count of vector registers used;
265                // skip the SSE spill when it is zero, exactly as compiled
266                // C prologues do.
267                "test al, al",
268                "je 2f",
269                "movaps [rsp + 48], xmm0",
270                "movaps [rsp + 64], xmm1",
271                "movaps [rsp + 80], xmm2",
272                "movaps [rsp + 96], xmm3",
273                "movaps [rsp + 112], xmm4",
274                "movaps [rsp + 128], xmm5",
275                "movaps [rsp + 144], xmm6",
276                "movaps [rsp + 160], xmm7",
277                "2:",
278                // Build the tag: gp_offset opens at 8 per named GP
279                // argument, fp_offset at 48 (no entry has named SSE
280                // arguments), overflow_arg_area at the first caller stack
281                // slot (entry rsp + 8, which is rsp + 208 now), and
282                // reg_save_area at the frame base.
283                concat!("mov dword ptr [rsp + 176], ", $gp),
284                "mov dword ptr [rsp + 180], 48",
285                "lea rax, [rsp + 208]",
286                "mov [rsp + 184], rax",
287                "mov [rsp + 192], rsp",
288                // The named arguments still sit in their registers; hand
289                // over the tag's address in the next slot and let the
290                // implementation run.
291                concat!("lea ", $tag_reg, ", [rsp + 176]"),
292                "call {imp}",
293                // The return value rides through rax (or xmm0) untouched.
294                "add rsp, 200",
295                "ret",
296                imp = sym $imp,
297            )
298        }
299    };
300}
301
302// Path-based imports for the crate's own callers and for the taproot
303// cdylib crate; `#[macro_export]` also places both at the crate root,
304// which is the `$crate` path the expansion rides on.
305#[doc(hidden)]
306pub use __vararg_entry_emit;
307pub use vararg_entry;
308
309#[cfg(test)]
310mod tests {
311    // `vararg_entry!` needs no import: `macro_rules!` scoping already
312    // carries it through the rest of the file.
313    use super::VaListTag;
314    use libc::{c_char, c_int, c_long, c_uint, c_void};
315
316    // The implementation halves: ordinary `extern "C"` functions that
317    // receive the named arguments plus the tag the naked entry built.
318
319    unsafe extern "C" fn sum_longs_impl(count: c_int, tag: *mut VaListTag) -> c_long {
320        // SAFETY: the entry hands us a live tag; each fetch below matches
321        // one `c_long` the test call sites pass.
322        unsafe {
323            let tag = &mut *tag;
324            let mut acc: c_long = 0;
325            for _ in 0..count {
326                acc = acc * 10 + tag.arg::<c_long>();
327            }
328            acc
329        }
330    }
331
332    unsafe extern "C" fn grab_impl(out: *mut u64, count: c_int, tag: *mut VaListTag) -> c_int {
333        // SAFETY: the fetch script mirrors the call site in
334        // `widths_and_pointers_recover_exactly`, slot for slot, and `out`
335        // has room for all eight recovered values.
336        unsafe {
337            let tag = &mut *tag;
338            *out.add(0) = tag.arg::<c_int>() as i64 as u64;
339            *out.add(1) = u64::from(tag.arg::<c_uint>());
340            *out.add(2) = tag.arg::<u64>();
341            *out.add(3) = tag.arg::<*mut u8>() as u64;
342            *out.add(4) = tag.arg::<usize>() as u64;
343            *out.add(5) = tag.arg::<i64>() as u64;
344            *out.add(6) = tag.arg::<*const c_void>() as u64;
345            *out.add(7) = tag.arg::<isize>() as u64;
346        }
347        count
348    }
349
350    unsafe extern "C" fn fgrab_impl(
351        out: *mut f64,
352        count: c_int,
353        check: c_int,
354        tag: *mut VaListTag,
355    ) -> c_int {
356        // SAFETY: the callers pass `count` doubles and an `out` with room
357        // for them.
358        unsafe {
359            let tag = &mut *tag;
360            for i in 0..count {
361                *out.add(i as usize) = tag.arg::<f64>();
362            }
363        }
364        check
365    }
366
367    unsafe extern "C" fn named4_impl(
368        a: c_int,
369        b: c_int,
370        c: c_int,
371        d: c_int,
372        tag: *mut VaListTag,
373    ) -> c_long {
374        // SAFETY: the caller passes three `c_long`s after the four named
375        // ints.
376        let (v1, v2, v3) = unsafe {
377            let tag = &mut *tag;
378            (
379                tag.arg::<c_long>(),
380                tag.arg::<c_long>(),
381                tag.arg::<c_long>(),
382            )
383        };
384        c_long::from(a)
385            + 2 * c_long::from(b)
386            + 3 * c_long::from(c)
387            + 4 * c_long::from(d)
388            + 1_000 * v1
389            + 1_000_000 * v2
390            + 1_000_000_000 * v3
391    }
392
393    unsafe extern "C" fn mixed_impl(iout: *mut i64, fout: *mut f64, tag: *mut VaListTag) -> c_int {
394        // SAFETY: the fetch script mirrors the call site in
395        // `mixed_classes_share_one_overflow_cursor`: four register ints,
396        // eight register doubles, then ints five and six and the ninth
397        // double off the shared overflow cursor, in call order.
398        unsafe {
399            let tag = &mut *tag;
400            for i in 0..4 {
401                *iout.add(i) = tag.arg::<i64>();
402            }
403            for i in 0..8 {
404                *fout.add(i) = tag.arg::<f64>();
405            }
406            *iout.add(4) = tag.arg::<i64>();
407            *iout.add(5) = tag.arg::<i64>();
408            *fout.add(8) = tag.arg::<f64>();
409        }
410        0
411    }
412
413    unsafe extern "C" fn execle_shape_impl(out: *mut *const c_char, tag: *mut VaListTag) -> c_int {
414        // SAFETY: the fetch script is the execle loop shape its call site
415        // mirrors: `*const c_char`s up to and including a null terminator,
416        // then one trailing envp-style pointer, and `out` has room for all
417        // of them.
418        unsafe {
419            let tag = &mut *tag;
420            let mut count = 0;
421            loop {
422                let ptr = tag.arg::<*const c_char>();
423                *out.add(count) = ptr;
424                count += 1;
425                if ptr.is_null() {
426                    break;
427                }
428            }
429            *out.add(count) = tag.arg::<*const *const c_char>().cast();
430            count as c_int
431        }
432    }
433
434    // The generated naked entries, one per named-argument count 1..=4.
435
436    vararg_entry! {
437        #[no_mangle]
438        unsafe extern "C" fn __taproot_va_test_sum_longs(count: c_int, ...) -> c_long
439            => sum_longs_impl
440    }
441
442    vararg_entry! {
443        #[no_mangle]
444        unsafe extern "C" fn __taproot_va_test_grab(out: *mut u64, count: c_int, ...) -> c_int
445            => grab_impl
446    }
447
448    vararg_entry! {
449        #[no_mangle]
450        unsafe extern "C" fn __taproot_va_test_fgrab(
451            out: *mut f64,
452            count: c_int,
453            check: c_int,
454            ...
455        ) -> c_int => fgrab_impl
456    }
457
458    vararg_entry! {
459        #[no_mangle]
460        unsafe extern "C" fn __taproot_va_test_named4(
461            a: c_int,
462            b: c_int,
463            c: c_int,
464            d: c_int,
465            ...
466        ) -> c_long => named4_impl
467    }
468
469    vararg_entry! {
470        #[no_mangle]
471        unsafe extern "C" fn __taproot_va_test_mixed(iout: *mut i64, fout: *mut f64, ...) -> c_int
472            => mixed_impl
473    }
474
475    vararg_entry! {
476        #[no_mangle]
477        unsafe extern "C" fn __taproot_va_test_execle_shape(
478            out: *mut *const c_char,
479            ...
480        ) -> c_int => execle_shape_impl
481    }
482
483    /// The C-side view of the entries above. Living in a child module
484    /// keeps these declarations from colliding with the definitions' item
485    /// names; the linker pairs them up by symbol. Calling through these
486    /// makes the tests real-ABI round trips: rustc performs the caller
487    /// half of the variadic protocol (register assignment, AL, stack
488    /// slots), and the naked entries must undo it exactly.
489    mod decl {
490        use libc::{c_char, c_int, c_long};
491
492        unsafe extern "C" {
493            pub fn __taproot_va_test_sum_longs(count: c_int, ...) -> c_long;
494            pub fn __taproot_va_test_grab(out: *mut u64, count: c_int, ...) -> c_int;
495            pub fn __taproot_va_test_fgrab(
496                out: *mut f64,
497                count: c_int,
498                check: c_int,
499                ...
500            ) -> c_int;
501            pub fn __taproot_va_test_named4(
502                a: c_int,
503                b: c_int,
504                c: c_int,
505                d: c_int,
506                ...
507            ) -> c_long;
508            pub fn __taproot_va_test_mixed(iout: *mut i64, fout: *mut f64, ...) -> c_int;
509            pub fn __taproot_va_test_execle_shape(out: *mut *const c_char, ...) -> c_int;
510        }
511    }
512
513    #[test]
514    fn ints_cross_into_the_overflow_area() {
515        // One named plus eight variadic longs: five ride rsi..r9, and the
516        // last three come off the caller's stack through
517        // `overflow_arg_area`. The positional fold proves both the values
518        // and their order.
519        let got = unsafe {
520            decl::__taproot_va_test_sum_longs(
521                8,
522                1 as c_long,
523                2 as c_long,
524                3 as c_long,
525                4 as c_long,
526                5 as c_long,
527                6 as c_long,
528                7 as c_long,
529                8 as c_long,
530            )
531        };
532        assert_eq!(got, 12_345_678);
533    }
534
535    #[test]
536    fn no_fp_call_walks_registers_only() {
537        // No doubles anywhere, so rustc sets AL = 0 and the entry must
538        // skip the SSE spill entirely.
539        let got = unsafe { decl::__taproot_va_test_sum_longs(2, 40 as c_long, 2 as c_long) };
540        assert_eq!(got, 402);
541    }
542
543    #[test]
544    fn widths_and_pointers_recover_exactly() {
545        let mut in_reg: u8 = 0;
546        let on_stack: u16 = 0;
547        let preg: *mut u8 = &mut in_reg;
548        let pstack: *const c_void = (&on_stack as *const u16).cast();
549        let mut out = [0_u64; 8];
550        // Two named plus eight variadic: slots one to four ride rdx, rcx,
551        // r8 and r9; five to eight cross into the overflow area, with a
552        // pointer landing on each side of the boundary.
553        let echoed = unsafe {
554            decl::__taproot_va_test_grab(
555                out.as_mut_ptr(),
556                8,
557                -7_i32,
558                0xDEAD_BEEF_u32,
559                0x8000_0000_0000_0001_u64,
560                preg,
561                usize::MAX - 41,
562                i64::MIN + 2,
563                pstack,
564                isize::MIN + 9,
565            )
566        };
567        assert_eq!(echoed, 8);
568        assert_eq!(out[0], -7_i64 as u64);
569        assert_eq!(out[1], 0xDEAD_BEEF);
570        assert_eq!(out[2], 0x8000_0000_0000_0001);
571        assert_eq!(out[3], preg as u64);
572        assert_eq!(out[4], (usize::MAX - 41) as u64);
573        assert_eq!(out[5], (i64::MIN + 2) as u64);
574        assert_eq!(out[6], pstack as u64);
575        assert_eq!(out[7], (isize::MIN + 9) as u64);
576    }
577
578    #[test]
579    fn f64s_ride_xmm0_to_7_then_spill() {
580        const DOUBLES: [f64; 10] = [
581            0.5,
582            -3.25,
583            1.0e300,
584            f64::MIN_POSITIVE,
585            -0.0,
586            6.022e23,
587            9_007_199_254_740_992.0,
588            -1.0,
589            9.75,
590            1_234.5,
591        ];
592        let mut out = [0.0_f64; 10];
593        // Three named ints, then ten doubles: xmm0..7 carry eight of them
594        // (rustc sets AL = 8) and the last two arrive through the
595        // overflow area.
596        let echoed = unsafe {
597            decl::__taproot_va_test_fgrab(
598                out.as_mut_ptr(),
599                10,
600                0x5AFE,
601                DOUBLES[0],
602                DOUBLES[1],
603                DOUBLES[2],
604                DOUBLES[3],
605                DOUBLES[4],
606                DOUBLES[5],
607                DOUBLES[6],
608                DOUBLES[7],
609                DOUBLES[8],
610                DOUBLES[9],
611            )
612        };
613        assert_eq!(echoed, 0x5AFE);
614        // Bit equality, so the negative zero must survive too.
615        for (got, want) in out.iter().zip(DOUBLES) {
616            assert_eq!(got.to_bits(), want.to_bits());
617        }
618    }
619
620    #[test]
621    fn f64s_partial_register_walk() {
622        let mut out = [0.0_f64; 3];
623        // AL = 3: the entry still spills all eight SSE registers, but the
624        // walker only reads back the three that carry arguments.
625        let echoed =
626            unsafe { decl::__taproot_va_test_fgrab(out.as_mut_ptr(), 3, 7, 1.5, 2.5, -8.125) };
627        assert_eq!(echoed, 7);
628        assert_eq!(out, [1.5, 2.5, -8.125]);
629    }
630
631    #[test]
632    fn four_named_args_start_the_walk_at_32() {
633        // Four named ints occupy rdi..rcx, so gp_offset opens at 32: the
634        // first two variadic longs sit in r8 and r9, the third on the
635        // stack.
636        let got = unsafe {
637            decl::__taproot_va_test_named4(10, 20, 30, 40, 7 as c_long, 8 as c_long, 9 as c_long)
638        };
639        assert_eq!(got, 9_008_007_300);
640    }
641
642    #[test]
643    fn mixed_classes_share_one_overflow_cursor() {
644        let mut iout = [0_i64; 6];
645        let mut fout = [0.0_f64; 9];
646        // Six GP arguments fill rdi..r9 (two named pointers plus four
647        // variadic ints), so ints five and six go to the stack. Eight
648        // doubles fill xmm0..7, so the ninth goes to the stack after
649        // them. The overflow area therefore reads back, in call order:
650        // 55, 66, 8.5.
651        let rc = unsafe {
652            decl::__taproot_va_test_mixed(
653                iout.as_mut_ptr(),
654                fout.as_mut_ptr(),
655                11_i64,
656                22_i64,
657                33_i64,
658                44_i64,
659                0.5,
660                1.5,
661                2.5,
662                3.5,
663                4.5,
664                5.5,
665                6.5,
666                7.5,
667                55_i64,
668                66_i64,
669                8.5,
670            )
671        };
672        assert_eq!(rc, 0);
673        assert_eq!(iout, [11, 22, 33, 44, 55, 66]);
674        assert_eq!(fout, [0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5]);
675    }
676
677    #[test]
678    fn the_execle_walk_collects_until_null_then_envp() {
679        // The execl/execle loop shape: one named argument, then `*const
680        // c_char`s until a null terminator, then (execle only) one more
681        // pointer. Six strings push the walk off the registers: rsi..r9
682        // carry the first five, and the sixth, the null and the envp
683        // pointer all come off the overflow area.
684        let argv = [
685            c"sh".as_ptr(),
686            c"-c".as_ptr(),
687            c"one".as_ptr(),
688            c"two".as_ptr(),
689            c"three".as_ptr(),
690            c"four".as_ptr(),
691        ];
692        let env = [c"X=1".as_ptr(), core::ptr::null()];
693        let envp: *const *const c_char = env.as_ptr();
694        let mut out = [core::ptr::null::<c_char>(); 8];
695        let walked = unsafe {
696            decl::__taproot_va_test_execle_shape(
697                out.as_mut_ptr(),
698                argv[0],
699                argv[1],
700                argv[2],
701                argv[3],
702                argv[4],
703                argv[5],
704                core::ptr::null::<c_char>(),
705                envp,
706            )
707        };
708        assert_eq!(walked, 7);
709        assert_eq!(&out[..6], &argv);
710        assert!(out[6].is_null());
711        assert_eq!(out[7], envp.cast::<c_char>());
712    }
713}