x86 32-bitowa (i386) funkcja kodu maszynowego, 13 bajtów
Konwencja wywoływania : i386 System V (stos argumentów), ze wskaźnikiem NULL jako wartownikiem / terminatorem listy końca argumentu . (Clobbers EDI, poza tym zgodny z SysV).
C (i asm) nie przekazują informacji o typie do funkcji variadic, więc opis OP dotyczący przekazywania liczb całkowitych lub tablic bez wyraźnych informacji o typie może być zaimplementowany tylko w konwencji, która przekazała jakiś obiekt struct / class (lub wskaźniki do takich ), a nie same liczby całkowite na stosie. Postanowiłem więc założyć, że wszystkie argumenty nie były wskaźnikami NULL, a obiekt wywołujący przechodzi terminator NULL.
Lista wskaźników zakończona wartością NULL argumentów jest faktycznie używana w C dla funkcji takich jak POSIXexecl(3)
: int execl(const char *path, const char *arg, ... /* (char *) NULL */);
C nie zezwala na int foo(...);
prototypy bez ustalonego arg, ale int foo();
oznacza to samo: args nieokreślony. (W przeciwieństwie do C ++, gdzie to oznacza int foo(void)
). W każdym razie jest to odpowiedź asm. Koncentrowanie kompilatora C w celu bezpośredniego wywołania tej funkcji jest interesujące, ale nie wymagane.
nasm -felf32 -l/dev/stdout arg-count.asm
z usuniętymi niektórymi wierszami komentarza.
24 global argcount_pointer_loop
25 argcount_pointer_loop:
26 .entry:
28 00000000 31C0 xor eax, eax ; search pattern = NULL
29 00000002 99 cdq ; counter = 0
30 00000003 89E7 mov edi, esp
31 ; scasd ; edi+=4; skip retaddr
32 .scan_args:
33 00000005 42 inc edx
34 00000006 AF scasd ; cmp eax,[edi] / edi+=4
35 00000007 75FC jne .scan_args
36 ; dec edx ; correct for overshoot: don't count terminator
37 ; xchg eax,edx
38 00000009 8D42FE lea eax, [edx-2] ; terminator + ret addr
40 0000000C C3 ret
size = 0D db $ - .entry
Pytanie pokazuje, że funkcja musi mieć możliwość zwrócenia 0, i postanowiłem spełnić to wymaganie, nie włączając końcowego wskaźnika NULL do liczby argumentów. Jednak kosztuje to 1 bajt. (W przypadku wersji 12-bajtowej usuń LEA i odkomentuj scasd
zewnętrzną pętlę i xchg
, ale nie dec edx
. Użyłem LEA, ponieważ kosztuje tyle samo, co pozostałe trzy instrukcje razem, ale jest bardziej wydajna, więc funkcja jest mniejsza ups.)
Osoba dzwoniąca C do testowania :
Zbudowany z:
nasm -felf32 -l /dev/stdout arg-count.asm | cut -b -28,$((28+12))- &&
gcc -Wall -O3 -g -std=gnu11 -m32 -fcall-used-edi arg-count.c arg-count.o -o ac &&
./ac
-fcall-used-edi
jest wymagane nawet przy -O0, aby powiedzieć gcc, aby zakładał, że funkcje zamykają się edi
bez zapisywania / przywracania, ponieważ użyłem tak wielu wywołań w jednej instrukcji C ( printf
wywołanie), że nawet -O0
używał EDI. Wydaje się, że gcc może bezpiecznie main
zablokować EDI z własnego obiektu wywołującego (w kodzie CRT), w Linuksie z glibc, ale poza tym mieszanie / dopasowanie kodu skompilowanego z innym jest całkowicie fałszywe -fcall-used-reg
. Nie ma żadnej __attribute__
wersji, która pozwalałaby nam zadeklarować funkcje asm z niestandardowymi konwencjami wywoływania innymi niż zwykle.
#include <stdio.h>
int argcount_rep_scas(); // not (...): ISO C requires at least one fixed arg
int argcount_pointer_loop(); // if you declare args at all
int argcount_loopne();
#define TEST(...) printf("count=%d = %d = %d (scasd/jne) | (rep scas) | (scas/loopne)\n", \
argcount_pointer_loop(__VA_ARGS__), argcount_rep_scas(__VA_ARGS__), \
argcount_loopne(__VA_ARGS__))
int main(void) {
TEST("abc", 0);
TEST(1, 1, 1, 1, 1, 1, 1, 0);
TEST(0);
}
Dwie inne wersje również miały 13 bajtów: ta oparta na loopne
wartości zwraca wartość zbyt wysoką o 1.
45 global argcount_loopne
46 argcount_loopne:
47 .entry:
49 00000010 31C0 xor eax, eax ; search pattern = NULL
50 00000012 31C9 xor ecx, ecx ; counter = 0
51 00000014 89E7 mov edi, esp
52 00000016 AF scasd ; edi+=4; skip retaddr
53 .scan_args:
54 00000017 AF scasd
55 00000018 E0FD loopne .scan_args
56 0000001A 29C8 sub eax, ecx
58 0000001C C3 ret
size = 0D = 13 bytes db $ - .entry
Ta wersja używa rep scasd zamiast pętli, ale pobiera modulo 256 zliczania argów (lub ograniczona do 256, jeśli górne bajty ecx
są 0 przy wejściu!)
63 ; return int8_t maybe?
64 global argcount_rep_scas
65 argcount_rep_scas:
66 .entry:
67 00000020 31C0 xor eax, eax
68 ; lea ecx, [eax-1]
69 00000022 B1FF mov cl, -1
70 00000024 89E7 mov edi, esp
71 ; scasd ; skip retaddr
72 00000026 F2AF repne scasd ; ecx = -len - 2 (including retaddr)
73 00000028 B0FD mov al, -3
74 0000002A 28C8 sub al, cl ; eax = -3 +len + 2
75 ; dec eax
76 ; dec eax
77 0000002C C3 ret
size = 0D = 13 bytes db $ - .entry
Zabawne jest, że kolejna wersja oparta na inc eax
/ pop edx
/ test edx,edx
/ jnz
ma 13 bajtów. Jest to konwencja callee-pops, która nigdy nie jest wykorzystywana przez implementacje języka C do funkcji variadic. (Wstawiłem add add do ecx, a jmp ecx zamiast ret. (Lub push / ret, aby nie zepsuć stosu predykcji adresu zwrotnego).