x86 32位(i386)机器代码功能,13个字节
调用约定:i386 System V(堆栈args),其中NULL指针作为arg-list的末尾/终止符。(Clobbers EDI,否则符合SysV)。
C(和asm)不会将类型信息传递给可变参数函数,因此OP传递不带显式类型信息的整数或数组的描述只能在传递某种struct / class对象(或指向此类的指针)的约定中实现),而不是堆栈上的裸整数。因此,我决定假定所有的args都是非NULL指针,并且调用方传递一个NULL终止符。
NULL终止的args指针列表实际上在C中用于POSIX之类的execl(3)
功能: int execl(const char *path, const char *arg, ... /* (char *) NULL */);
C不允许int foo(...);
没有固定arg的原型,但int foo();
含义相同:args未指定。(不同于C ++中的含义int foo(void)
)。无论如何,这是一个asm答案。哄骗C编译器直接调用此函数很有趣,但不是必需的。
nasm -felf32 -l/dev/stdout arg-count.asm
删除了一些注释行。
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
问题表明该函数必须能够返回0,我决定遵循这一要求,在arg计数中不包括终止NULL指针。不过,这确实花费了1个字节。(对于12字节的版本,删除LEA并取消注释scasd
循环外部和xchg
,但不要注释dec edx
。我使用LEA是因为它的成本与其他三条指令的总和相同,但效率更高,因此功能更少哎呀。)
C调用者进行测试:
内置:
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
即使在-O0也是必需的,以告诉gcc假设函数edi
不保存或恢复它,因为我在一个C语句(printf
调用)中使用了那么多调用,甚至-O0
使用EDI。对于gcc main
,在Linux上使用glibc掩盖来自其自己的调用方的EDI(在CRT代码中)似乎是安全的,但否则,将使用different编译的代码混合/匹配完全是伪造的-fcall-used-reg
。没有任何__attribute__
版本可以让我们使用不同于通常的自定义调用约定来声明asm函数。
#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);
}
另外两个版本还有13个字节:这个基于的版本loopne
返回的值太大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
此版本使用rep scasd而不是循环,但是将arg计数取模256。(如果输入的高字节为ecx
0,则上限为256 !)
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
有趣的是,另一个基于inc eax
/ pop edx
/ test edx,edx
/的版本jnz
以13个字节出现。这是callee-pops约定,C实现从未将其用于可变函数。(我将ret addr弹出到ecx中,然后将jmp ecx弹出到ret中。(或者通过push / ret不会破坏返回地址预测变量堆栈)。