这个没有libc的C程序是如何工作的?
我遇到了一个不使用libc编写的最小HTTP服务器: 我可以看到基本的字符串处理函数已经定义,导致这个没有libc的C程序是如何工作的?,c,assembly,x86-64,system-calls,abi,C,Assembly,X86 64,System Calls,Abi,我遇到了一个不使用libc编写的最小HTTP服务器: 我可以看到基本的字符串处理函数已经定义,导致writesyscall: #define fprint(fd, s) write(fd, s, strlen(s)) #define fprintn(fd, s, n) write(fd, s, n) #define fprintl(fd, s) fprintn(fd, s, sizeof(s) - 1) #define fprintln(fd, s) fprintl(fd, s "\n
write
syscall:
#define fprint(fd, s) write(fd, s, strlen(s))
#define fprintn(fd, s, n) write(fd, s, n)
#define fprintl(fd, s) fprintn(fd, s, sizeof(s) - 1)
#define fprintln(fd, s) fprintl(fd, s "\n")
#define print(s) fprint(1, s)
#define printn(s, n) fprintn(1, s, n)
#define printl(s) fprintl(1, s)
#define println(s) fprintln(1, s)
基本系统调用在C文件中声明:
size_t read(int fd, void *buf, size_t nbyte);
ssize_t write(int fd, const void *buf, size_t nbyte);
int open(const char *path, int flags);
int close(int fd);
int socket(int domain, int type, int protocol);
int accept(int socket, sockaddr_in_t *restrict address,
socklen_t *restrict address_len);
int shutdown(int socket, int how);
int bind(int socket, const sockaddr_in_t *address, socklen_t address_len);
int listen(int socket, int backlog);
int setsockopt(int socket, int level, int option_name, const void *option_value,
socklen_t option_len);
int fork();
void exit(int status);
因此,我猜魔法发生在start.S
中,它包含\u start
和一种特殊的系统调用编码方式,通过创建全局标签并在r9中累积值来保存字节:
.intel_syntax noprefix
/* functions: rdi, rsi, rdx, rcx, r8, r9 */
/* syscalls: rdi, rsi, rdx, r10, r8, r9 */
/* ^^^ */
/* stack grows from a high address to a low address */
#define c(x, n) \
.global x; \
x:; \
add r9,n
c(exit, 3) /* 60 */
c(fork, 3) /* 57 */
c(setsockopt, 4) /* 54 */
c(listen, 1) /* 50 */
c(bind, 1) /* 49 */
c(shutdown, 5) /* 48 */
c(accept, 2) /* 43 */
c(socket, 38) /* 41 */
c(close, 1) /* 03 */
c(open, 1) /* 02 */
c(write, 1) /* 01 */
.global read /* 00 */
read:
mov r10,rcx
mov rax,r9
xor r9,r9
syscall
ret
.global _start
_start:
xor rbp,rbp
xor r9,r9
pop rdi /* argc */
mov rsi,rsp /* argv */
call main
call exit
这种理解正确吗?GCC使用start.S
中定义的符号进行系统调用,然后程序在\u start
中启动,并从C文件调用main
另外,单独的
httpd.asm
自定义二进制文件是如何工作的?只需结合C源代码和启动程序集进行手动优化程序集?您对所发生的事情的理解非常正确。很有趣,我以前从未见过这样的事情。但基本上正如您所说,每次它调用标签时,正如您所说,r9
不断累积,直到它达到read
,其系统调用号为0。这就是为什么这个命令非常聪明。假设调用read
之前r9
为0(调用正确的系统调用之前read
标签本身为零r9
),则无需添加,因为r9
已经具有所需的正确系统调用号write
的系统调用号是1,因此只需要从0中添加1,这在宏调用中显示open
的系统调用号是2,因此首先在open
标签上加1,然后在write
标签上再加1,然后在read
标签上将正确的系统调用号放入rax
。等等参数寄存器,如rdi
、rsi
、rdx
等,也不会被触动,因此它基本上起到了正常函数调用的作用
另外,单独的httpd.asm自定义二进制文件是如何工作的?结合C源代码和启动程序集的手动优化程序集
我想你是在说什么。不确定这里到底发生了什么,但看起来像是手动创建了一个ELF文件,可能是为了进一步减小大小。(我克隆了repo并调整了.c和.S,以更好地使用clang-Oz:992字节进行编译,而不是使用gcc编译最初的1208字节。请参阅我的fork中的,直到我开始清理并发送拉取请求。使用clang时,系统调用的内联asm确实可以节省总体大小,特别是在main没有调用且没有rets.IDK的情况下,如果我想使用它的话。)在从编译器输出重新生成后,重新生成整个.asm
;当然也有大量的代码可以节省,例如在循环中使用lodsb
)
在调用这些标签之前,它们需要
r9
为0
,或者使用寄存器全局变量,或者可能。否则GCC会像其他寄存器一样,在r9
中留下任何垃圾
它们的函数是用普通原型来声明的,而不是用伪0
args来声明6个args,以使每个调用站点实际归零r9
,所以它们不是这样做的
编码系统调用的特殊方法 我不会将其描述为“对系统调用进行编码”。可能是“定义系统调用包装函数”。它们为每个系统调用定义自己的包装函数,以一种优化的方式,在底部的一个公共处理程序中。在C编译器的asm输出中,您仍然会看到
调用编写
(如果最终二进制文件使用内联asm让编译器将syscall
指令与正确寄存器中的args内联,而不是使其看起来像一个普通函数,对所有被调用的寄存器都进行重载,那么它可能会更为紧凑。特别是如果使用clang-Oz
编译,它将使用3字节>按2而不是5字节的mov eax,2来设置呼叫号码。push imm8
/pop
/syscall
与call rel32
大小相同)
是的,您可以使用手工编写的asm来定义函数。global foo
/foo:
您可以将其视为一个大型函数,具有不同系统调用的多个入口点。在asm中,执行总是传递到下一条指令,而不管标签如何,除非您使用跳转/调用/ret指令。CP你不知道标签
所以它就像一个Cswitch(){}
语句,不带中断;
介于大小写之间:
标签,或者类似于C标签,您可以使用goto
跳转到。当然,除了在asm中,您可以在全局范围内执行此操作,而在C中,您只能在函数内执行goto操作。在asm中,您可以调用而不仅仅是goto
(jmp
)
或者,如果您将其重新构建为一个tailcalls链,我们甚至可以省略jmp foo
,而仅仅是失败:如果您有足够聪明的编译器,像这样的C确实可以编译成手工编写的asm。(并且您可以解决arg类型。)
register long callnum asm("r9"); // GCC extension
long open(args...) {
callnum++;
return write(args...);
}
long write(args...) {
callnum++;
return read(args...); // tailcall
}
long read(args...){
tmp=callnum;
callnum=0; // reset callnum for next call
return syscall(tmp, args...);
}
args…
是arg传递寄存器(RDI、RSI、RDX、RCX、R8)R9是x86-64系统V的最后一个通过arg的寄存器,但他们没有使用任何接受6个arg的系统调用。setsockopt
接受5个arg,因此他们不能跳过mov r10,rcx
。但是他们可以将R9用于其他对象,而不需要它来通过第6个arg
有趣的是,他们如此努力地以牺牲性能为代价来节省字节,但仍然使用。除非他们使用gcc-Wa,-Os start.s
,否则GAS不会为您优化REX前缀。()
他们可以用xchg-rax,r9
(包括REX在内的2个字节)而不是mov-rax,r9保存另一个字节
register long callnum asm("r9"); // GCC extension
long open(args...) {
callnum++;
return write(args...);
}
long write(args...) {
callnum++;
return read(args...); // tailcall
}
long read(args...){
tmp=callnum;
callnum=0; // reset callnum for next call
return syscall(tmp, args...);
}