Problem attractor
stackoverflow There is this problem. The standard says that the variable parameters of makecontext must be of type int, and then find the variable parameters of makecontext Source code There is a note in the following paragraph.
/* Handle arguments. The standard says the parameters must all be int values. This is an historic accident and would be done differently today. For x86-64 all integer values are passed as 64-bit values and therefore extending the API to copy 64-bit values instead of 32-bit ints makes sense. It does not break existing functionality and it does not violate the standard which says that passing non-int values means undefined behavior. */
In the notes, it is mentioned that the shaping of variable parameters is treated as 64bit. Let's explore the implementation principle of x64 variable parameters, including the situation that floating-point numbers are contained in variable parameters.
Experimental code
The experimental code is as follows
#include <stdio.h> #include <stdarg.h> int f(int x, float y, short a, double b, ...) { va_list ap; va_start(ap, b); char cc = va_arg(ap, int); float dd = va_arg(ap, double); int ee = va_arg(ap, int); double ff = va_arg(ap, double); int last = va_arg(ap, int); printf("%d %f %d %lf %d\n", (int)cc, dd, ee, ff, last); for(int i = 0; i < last; ++i) { int tmp = va_arg(ap, int); printf("%d\n", tmp); } va_end(ap); } int main() { f(1, 2.2f, 3, 4.4, 5, 6.6f, 7, 8.8, 3, 10, 11, 12, 'x', 'y'); return 0; }
va_list structure
typedef struct { unsigned int gp_offset; unsigned int fp_offset; void *overflow_arg_area; void *reg_save_area; } va_list[1];
g represents a general-purpose register and f represents a floating-point register.
reg_save_area points to a special location on the stack, which is in the middle of the stack. 6 + 8 positions are provided here (the first 6 positions are used to store the parameters passed by the general register, 8 bytes each; the last 8 positions are used to store the floating point parameters passed by the xmm register, 16 bytes each). All variable parameters passed through registers are eventually copied here. For example, the 5th and 6th parameters (variable parameters) are passed through the general register. Then the 5th and 6th positions will be occupied, and the first 4 positions will not be used. If no variable parameter is passed through the register (i.e. there are no less than 6 determined parameters before the variable parameter), these 6 positions still exist (occupy space) but are not used.
gp_offset indicates that the variable parameters stored in the six positions mentioned above are relative to overflow_ arg_ Offset of area. If it is a variable parameter from the third number, the offset is 2 * 8 = = 16, that is, 0x10 in the following figure
overflow_arg_area represents the starting address of the parameter passed through the stack. The first 6 parameters are passed through registers together with the determined parameters and variable parameters. The following are passed through the stack. The parameters passed in the last stack are put on the stack first, and the seventh parameter is put on the stack last, with the lowest address. So overflow_arg_area points to the seventh parameter.
Under x64, floating point numbers are passed through the xmm register. The variable parameters passed by the xmm floating-point register will eventually be copied to the stack. fp_offset indicates that the starting address of the variable floating-point number parameter copied to the stack deviates from reg_ save_ The size of area, because the parameters passed by 6 8-byte general-purpose registers are just separated in the middle. Therefore, if you start from the third floating point number (excluding non floating point numbers), it is 8 * 6 + 2 * 16 = 80, that is, 0x50 in the following figure
So the principle is through reg_save_area + gp_offset accesses the variable parameters passed through the general register. These variable parameters are copied to the middle of the stack through reg_save_area + fp_offset accesses the variable parameters passed by the floating-point register, and these variable parameters are also copied to the middle of the stack. The remaining parameters are passed to the caller's stack by the superior caller of this function through the stack and through overflow_arg_area to access.
On stack memory layout
Assembly code
The corresponding assembly code and comments are as follows
gef➤ disassemble /m main Dump of assembler code for function main: 26 { => 0x0000000000400854 <+0>: push %rbp 0x0000000000400855 <+1>: mov %rsp,%rbp 27 f(1, 2.2f, 3, 4.4, 5, 6.6f, 7, 8.8, 3, 10, 11, 12); 0x0000000000400858 <+4>: movsd 0x118(%rip),%xmm2 # 0x400978 0x0000000000400860 <+12>: movsd 0x118(%rip),%xmm1 # 0x400980 0x0000000000400868 <+20>: movsd 0x118(%rip),%xmm0 # 0x400988 0x0000000000400870 <+28>: pushq $0xc 0x0000000000400872 <+30>: pushq $0xb 0x0000000000400874 <+32>: mov $0xa,%r9d #Pass the 6th non floating point parameter to the general register 0x000000000040087a <+38>: mov $0x3,%r8d #Pass the 5th non floating point parameter to the general register 0x0000000000400880 <+44>: movapd %xmm2,%xmm3 0x0000000000400884 <+48>: mov $0x7,%ecx #Pass the 4th non floating point parameter to the general register 0x0000000000400889 <+53>: movapd %xmm1,%xmm2 0x000000000040088d <+57>: mov $0x5,%edx #Pass the third non floating point parameter to the general register 0x0000000000400892 <+62>: movapd %xmm0,%xmm1 #The XMM register is used to pass floating-point parameters. Before the call instruction, pass the floating-point number parameters from left to right starting from xmm0 in order 0x0000000000400896 <+66>: mov $0x3,%esi #Pass the second non floating point parameter to the general register 0x000000000040089b <+71>: movss 0xed(%rip),%xmm0 # 0x400990 0x00000000004008a3 <+79>: mov $0x1,%edi #Pass the first non floating point parameter to the general register 0x00000000004008a8 <+84>: mov $0x4,%eax #Eax passes the number of floating point number parameters. Among callers, the value in eax is used to determine whether the floating-point register needs to be copied to the stack. 0x00000000004008ad <+89>: callq 0x400596 <f> #Of course, if there is no floating-point number parameter, MOV $0 is directly used,% eax so there is no need to copy 0x00000000004008b2 <+94>: add $0x10,%rsp 28 return 0; 0x00000000004008b6 <+98>: mov $0x0,%eax 29 } 0x00000000004008bb <+103>: leaveq 0x00000000004008bc <+104>: retq gef➤ disassemble /m f Dump of assembler code for function f: 4 { 0x0000000000400596 <+0>: push %rbp 0x0000000000400597 <+1>: mov %rsp,%rbp 0x000000000040059a <+4>: sub $0x110,%rsp 0x00000000004005a1 <+11>: mov %edi,-0xf4(%rbp) #The first two parameters passed by the general register are named parameters, which are copied directly to the top of the stack 0x00000000004005a7 <+17>: movss %xmm0,-0xf8(%rbp) 0x00000000004005af <+25>: movsd %xmm1,-0x108(%rbp) #The first two floating-point parameters passed by xmm are also named parameters and are copied directly to the top of the stack 0x00000000004005b7 <+33>: mov %rdx,-0xa0(%rbp) 0x00000000004005be <+40>: mov %rcx,-0x98(%rbp) 0x00000000004005c5 <+47>: mov %r8,-0x90(%rbp) 0x00000000004005cc <+54>: mov %r9,-0x88(%rbp) #The last four parameters transferred by the general register are variable parameters. The variable parameters are copied to a separate specific area in its own stack to store the parameters transferred by the general register 0x00000000004005d3 <+61>: test %al,%al #The caller is responsible for setting the value of eax, indicating the number of all floating-point number parameters passed 0x00000000004005d5 <+63>: je 0x4005ef <f+89> 0x00000000004005d7 <+65>: movaps %xmm2,-0x60(%rbp) 0x00000000004005db <+69>: movaps %xmm3,-0x50(%rbp) #The remaining floating-point parameters passed by xmm are variable parameters, and the variable parameters are also copied to a specific area on its own stack that holds a separate floating-point register 0x00000000004005df <+73>: movaps %xmm4,-0x40(%rbp) 0x00000000004005e3 <+77>: movaps %xmm5,-0x30(%rbp) 0x00000000004005e7 <+81>: movaps %xmm6,-0x20(%rbp) # 0x00000000004005eb <+85>: movaps %xmm7,-0x10(%rbp) 0x00000000004005ef <+89>: mov %esi,%eax #Both edi and esi pass named parameters. So copy it directly to the top of the stack. 0x00000000004005f1 <+91>: mov %ax,-0xfc(%rbp) #The parameters passed by general registers and floating-point numbers are stored adjacent to each other on the stack 5 va_list ap; 6 va_start(ap, b); 0x00000000004005f8 <+98>: movl $0x10,-0xe8(%rbp) #va_list.gp_offset the offset of the variable parameter passed by the first general-purpose register from the saved register area 0x0000000000400602 <+108>: movl $0x50,-0xe4(%rbp) #va_list.fp_offset the offset of the variable parameter passed by the first floating-point register from the saved register area 0x000000000040060c <+118>: lea 0x10(%rbp),%rax #Here is rbp + indicating the caller's stack. 0x10 just skips the saved caller's rbp (the current rbp points to the old rbp) and return address 0x0000000000400610 <+122>: mov %rax,-0xe0(%rbp) #va_list.overflow_arg_area pointing 0x0000000000400617 <+129>: lea -0xb0(%rbp),%rax 0x000000000040061e <+136>: mov %rax,-0xd8(%rbp) #va_list.reg_save_area points to the register area saved on the previous stack frame (saving the parameters passed by the general-purpose register and floating-point register) 7 8 char cc = va_arg(ap, int); 0x0000000000400625 <+143>: mov -0xe8(%rbp),%eax #eax = va_list.gp_offset 0x000000000040062b <+149>: cmp $0x2f,%eax #Since the general register has only 6 positions and 48 bytes, whether the offset exceeds 47 is calculated here, which indicates that the remaining variable parameters are not in this special area in this stack frame 0x000000000040062e <+152>: ja 0x400653 <f+189> 0x0000000000400630 <+154>: mov -0xd8(%rbp),%rax #va_list.reg_save_area 0x0000000000400637 <+161>: mov -0xe8(%rbp),%edx 0x000000000040063d <+167>: mov %edx,%edx 0x000000000040063f <+169>: add %rdx,%rax #eax = va_list.gp_offset + va_list.reg_save_area, which just points to the parameters passed by the register saved in a specific area 0x0000000000400642 <+172>: mov -0xe8(%rbp),%edx 0x0000000000400648 <+178>: add $0x8,%edx 0x000000000040064b <+181>: mov %edx,-0xe8(%rbp) #va_list.gp_offset = va_list.gp_offset + 0x8, offset + 8 points to the next saved register parameter 0x0000000000400651 <+187>: jmp 0x400665 <f+207> 0x0000000000400653 <+189>: mov -0xe0(%rbp),%rax 0x000000000040065a <+196>: lea 0x8(%rax),%rdx 0x000000000040065e <+200>: mov %rdx,-0xe0(%rbp) 0x0000000000400665 <+207>: mov (%rax),%eax #eax points to the saved register parameter, and the value is 0x0000000000400667 <+209>: mov %al,-0xb5(%rbp) # Copy to the local variable cc on the stack 9 float dd = va_arg(ap, double); 0x000000000040066d <+215>: mov -0xe4(%rbp),%eax #eax = va_list.fp_offset 0x0000000000400673 <+221>: cmp $0xaf,%eax #General purpose register 6 * 8 floating point number register 8 * 16 is 48 + 128 = 128 bytes in total. If it is greater than 0xaf(127), it means that the remaining floating point number parameters are on the previous stack 0x0000000000400678 <+226>: ja 0x40069d <f+263> 0x000000000040067a <+228>: mov -0xd8(%rbp),%rax #va_list.reg_save_area 0x0000000000400681 <+235>: mov -0xe4(%rbp),%edx 0x0000000000400687 <+241>: mov %edx,%edx 0x0000000000400689 <+243>: add %rdx,%rax #rax = va_list.reg_save_area + va_list.fp_offset just points to the stored floating-point register in a special area 0x000000000040068c <+246>: mov -0xe4(%rbp),%edx 0x0000000000400692 <+252>: add $0x10,%edx 0x0000000000400695 <+255>: mov %edx,-0xe4(%rbp) #Offset + 16, pointing to the next va_list.fp_offset = va_list.fp_offset + 16 0x000000000040069b <+261>: jmp 0x4006af <f+281> 0x000000000040069d <+263>: mov -0xe0(%rbp),%rax 0x00000000004006a4 <+270>: lea 0x8(%rax),%rdx 0x00000000004006a8 <+274>: mov %rdx,-0xe0(%rbp) 0x00000000004006af <+281>: movsd (%rax),%xmm0 #These three instructions are copied to xmm0 with double precision first 0x00000000004006b3 <+285>: cvtsd2ss %xmm0,%xmm2 #Then convert double precision to single precision and copy it to xmm2 for transfer 0x00000000004006b7 <+289>: movss %xmm2,-0xbc(%rbp) #Finally, it is assigned to the local variable dd 10 int ee = va_arg(ap, int); 0x00000000004006bf <+297>: mov -0xe8(%rbp),%eax 0x00000000004006c5 <+303>: cmp $0x2f,%eax 0x00000000004006c8 <+306>: ja 0x4006ed <f+343> 0x00000000004006ca <+308>: mov -0xd8(%rbp),%rax 0x00000000004006d1 <+315>: mov -0xe8(%rbp),%edx 0x00000000004006d7 <+321>: mov %edx,%edx 0x00000000004006d9 <+323>: add %rdx,%rax 0x00000000004006dc <+326>: mov -0xe8(%rbp),%edx 0x00000000004006e2 <+332>: add $0x8,%edx 0x00000000004006e5 <+335>: mov %edx,-0xe8(%rbp) 0x00000000004006eb <+341>: jmp 0x4006ff <f+361> 0x00000000004006ed <+343>: mov -0xe0(%rbp),%rax 0x00000000004006f4 <+350>: lea 0x8(%rax),%rdx 0x00000000004006f8 <+354>: mov %rdx,-0xe0(%rbp) 0x00000000004006ff <+361>: mov (%rax),%eax 0x0000000000400701 <+363>: mov %eax,-0xc0(%rbp) 11 double ff = va_arg(ap, double); 0x0000000000400707 <+369>: mov -0xe4(%rbp),%eax 0x000000000040070d <+375>: cmp $0xaf,%eax 0x0000000000400712 <+380>: ja 0x400737 <f+417> 0x0000000000400714 <+382>: mov -0xd8(%rbp),%rax 0x000000000040071b <+389>: mov -0xe4(%rbp),%edx 0x0000000000400721 <+395>: mov %edx,%edx 0x0000000000400723 <+397>: add %rdx,%rax 0x0000000000400726 <+400>: mov -0xe4(%rbp),%edx 0x000000000040072c <+406>: add $0x10,%edx 0x000000000040072f <+409>: mov %edx,-0xe4(%rbp) 0x0000000000400735 <+415>: jmp 0x400749 <f+435> 0x0000000000400737 <+417>: mov -0xe0(%rbp),%rax 0x000000000040073e <+424>: lea 0x8(%rax),%rdx 0x0000000000400742 <+428>: mov %rdx,-0xe0(%rbp) 0x0000000000400749 <+435>: movsd (%rax),%xmm0 0x000000000040074d <+439>: movsd %xmm0,-0xc8(%rbp) 12 int last = va_arg(ap, int); 0x0000000000400755 <+447>: mov -0xe8(%rbp),%eax 0x000000000040075b <+453>: cmp $0x2f,%eax 0x000000000040075e <+456>: ja 0x400783 <f+493> 0x0000000000400760 <+458>: mov -0xd8(%rbp),%rax 0x0000000000400767 <+465>: mov -0xe8(%rbp),%edx 0x000000000040076d <+471>: mov %edx,%edx 0x000000000040076f <+473>: add %rdx,%rax 0x0000000000400772 <+476>: mov -0xe8(%rbp),%edx 0x0000000000400778 <+482>: add $0x8,%edx 0x000000000040077b <+485>: mov %edx,-0xe8(%rbp) 0x0000000000400781 <+491>: jmp 0x400795 <f+511> 0x0000000000400783 <+493>: mov -0xe0(%rbp),%rax 0x000000000040078a <+500>: lea 0x8(%rax),%rdx 0x000000000040078e <+504>: mov %rdx,-0xe0(%rbp) 0x0000000000400795 <+511>: mov (%rax),%eax 0x0000000000400797 <+513>: mov %eax,-0xcc(%rbp) 13 14 printf("%d %f %d %lf %d\n", (int)cc, dd, ee, ff, last); 0x000000000040079d <+519>: cvtss2sd -0xbc(%rbp),%xmm0 0x00000000004007a5 <+527>: movsbl -0xb5(%rbp),%eax 0x00000000004007ac <+534>: mov -0xcc(%rbp),%ecx 0x00000000004007b2 <+540>: movsd -0xc8(%rbp),%xmm1 0x00000000004007ba <+548>: mov -0xc0(%rbp),%edx 0x00000000004007c0 <+554>: mov %eax,%esi 0x00000000004007c2 <+556>: mov $0x400958,%edi 0x00000000004007c7 <+561>: mov $0x2,%eax 0x00000000004007cc <+566>: callq 0x4004a0 <printf@plt> 15 16 for(int i = 0; i < last; ++i) 0x00000000004007d1 <+571>: movl $0x0,-0xb4(%rbp) 0x00000000004007db <+581>: jmp 0x400843 <f+685> 0x000000000040083c <+678>: addl $0x1,-0xb4(%rbp) 0x0000000000400843 <+685>: mov -0xb4(%rbp),%eax 0x0000000000400849 <+691>: cmp -0xcc(%rbp),%eax 0x000000000040084f <+697>: jl 0x4007dd <f+583> 17 { 18 int tmp = va_arg(ap, int); 0x00000000004007dd <+583>: mov -0xe8(%rbp),%eax #eax = va_list.gp_offset 0x00000000004007e3 <+589>: cmp $0x2f,%eax #Is the stored on the stack exhausted 0x00000000004007e6 <+592>: ja 0x40080b <f+629> 0x00000000004007e8 <+594>: mov -0xd8(%rbp),%rax 0x00000000004007ef <+601>: mov -0xe8(%rbp),%edx 0x00000000004007f5 <+607>: mov %edx,%edx 0x00000000004007f7 <+609>: add %rdx,%rax 0x00000000004007fa <+612>: mov -0xe8(%rbp),%edx 0x0000000000400800 <+618>: add $0x8,%edx 0x0000000000400803 <+621>: mov %edx,-0xe8(%rbp) 0x0000000000400809 <+627>: jmp 0x40081d <f+647> 0x000000000040080b <+629>: mov -0xe0(%rbp),%rax #If you run out, jump here, eax = va_list.overflow_arg_area points to the previous stack frame 0x0000000000400812 <+636>: lea 0x8(%rax),%rdx # 0x0000000000400816 <+640>: mov %rdx,-0xe0(%rbp) #va_list.overflow_arg_area = va_list.overflow_arg_area + 8 0x000000000040081d <+647>: mov (%rax),%eax #Value 0x000000000040081f <+649>: mov %eax,-0xd0(%rbp) 19 printf("%d\n", tmp); 0x0000000000400825 <+655>: mov -0xd0(%rbp),%eax 0x000000000040082b <+661>: mov %eax,%esi 0x000000000040082d <+663>: mov $0x40096d,%edi 0x0000000000400832 <+668>: mov $0x0,%eax 0x0000000000400837 <+673>: callq 0x4004a0 <printf@plt> 20 } 21 22 va_end(ap); 23 } 0x0000000000400851 <+699>: nop 0x0000000000400852 <+700>: leaveq 0x0000000000400853 <+701>: retq
x64 variable parameters and general types are saved according to 8 bytes, Therefore, there is no need to be integers.
summary
1. Via va_arg to get the parameter value, generalized shaping (char, short...) The specified type length cannot be less than int, and the floating-point type cannot be float. Otherwise, there will be a warning, the type will be promoted, and there is a problem with the generated assembly code.
warning: 'char' is promoted to 'int' when passed through '...'
warning: 'float' is promoted to 'double' when passed through '...'
2. Only the variable parameters passed through the register will be assigned to this special area on the stack. After all, the named parameters are copied to the top of the stack. Redundant parameters (except 6 shaping parameters and 8 floating-point parameters) are put on the stack before the call instruction, that is, the function, as the callee, is on the caller's stack, and these shaping passed directly through the stack occupy 8 bytes. Floating point type, whether single precision or double precision, also occupy 8 bytes.
If it is called in the following way, because there are already 8 floating-point parameters in the front, 14.14f will be passed through the stack.
f(1, 2.2f, 3, 4.4, 5, 6.6f, 7, 8.8, 3, 10, 11, 12, 'x', 'y', 9.9, 10.1, 11.11, 12.12, 13.3f, 14.4f);
The corresponding part is disassembled as follows
0x00000000004008e4 <+0>: push %rbp 0x00000000004008e5 <+1>: mov %rsp,%rbp 0x00000000004008e8 <+4>: movsd 0x158(%rip),%xmm7 # 0x400a48 0x00000000004008f0 <+12>: movsd 0x158(%rip),%xmm6 # 0x400a50 0x00000000004008f8 <+20>: movsd 0x158(%rip),%xmm5 # 0x400a58 0x0000000000400900 <+28>: movsd 0x158(%rip),%xmm4 # 0x400a60 0x0000000000400908 <+36>: movsd 0x158(%rip),%xmm3 # 0x400a68 0x0000000000400910 <+44>: movsd 0x158(%rip),%xmm2 # 0x400a70 0x0000000000400918 <+52>: movsd 0x158(%rip),%xmm1 # 0x400a78 0x0000000000400920 <+60>: movsd 0x158(%rip),%xmm0 # 0x400a80 0x0000000000400928 <+68>: lea -0x8(%rsp),%rsp 0x000000000040092d <+73>: movsd %xmm0,(%rsp) #13.3f put on the stack 0x0000000000400932 <+78>: movsd 0x14e(%rip),%xmm0 # 0x400a88 0x000000000040093a <+86>: lea -0x8(%rsp),%rsp 0x000000000040093f <+91>: movsd %xmm0,(%rsp) # 0x400a88 #14.4f put on the stack 0x0000000000400944 <+96>: pushq $0x79 #Although char directly occupies 8 bytes 0x0000000000400946 <+98>: pushq $0x78 0x0000000000400948 <+100>: pushq $0xc 0x000000000040094a <+102>: pushq $0xb #Directly occupy 8 bytes 0x000000000040094c <+104>: mov $0xa,%r9d 0x0000000000400952 <+110>: mov $0x3,%r8d 0x0000000000400958 <+116>: mov $0x7,%ecx 0x000000000040095d <+121>: mov $0x5,%edx 0x0000000000400962 <+126>: mov $0x3,%esi 0x0000000000400967 <+131>: movss 0x121(%rip),%xmm0 # 0x400a90 #The first floating-point parameter 2.2f is passed to the register 0x000000000040096f <+139>: mov $0x1,%edi 0x0000000000400974 <+144>: mov $0x8,%eax 0x0000000000400979 <+149>: callq 0x400596 <f> 0x000000000040097e <+154>: add $0x30,%rsp 0x0000000000400982 <+158>: mov $0x0,%eax 0x0000000000400987 <+163>: leaveq 0x0000000000400988 <+164>: retq
Previously via VA_ When Arg gets the parameter, if it is on the caller's stack, whether it is integer or floating-point, va_list.overflow_arg_area is increased by 8 bytes, which also shows this point.
lea 0x8(%rax),%rdx mov %rdx,-0xe0(%rbp) #va_list.overflow_arg_area = va_list.overflow_arg_area + 8
Under x64, the integer and floating-point parameters passed through the stack occupy 8 bytes. After all, the pushq instruction is used. However, the named parameters passed through the register are copied to the stack, because the stack space is opened first, and then mov copied into the stack. They can be merged to save memory.
The variable parameters passed through the register are eventually copied to a specific area on the stack. The integer is 8 bytes, and the floating-point type is 16 bytes because it is an xmm register.