Warning: file_get_contents(/data/phpspider/zhask/data//catemap/0/assembly/6.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
这个没有libc的C程序是如何工作的?_C_Assembly_X86 64_System Calls_Abi - Fatal编程技术网

这个没有libc的C程序是如何工作的?

这个没有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

我遇到了一个不使用libc编写的最小HTTP服务器:

我可以看到基本的字符串处理函数已经定义,导致
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你不知道标签

所以它就像一个C
switch(){}
语句,不带
中断;
介于
大小写之间:
标签,或者类似于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...);
}