繁体   English   中英

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

[英]How does this C program without libc work?

我遇到了一个没有 libc 的最小 HTTP 服务器: https://github.com/Francesco149/nolibc-httpd

我可以看到定义了基本的字符串处理函数,导致了write系统调用:

#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中,它包含_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中定义的符号进行系统调用,然后程序在_start中启动并从 C 文件中调用main

另外,单独的httpd.asm自定义二进制文件是如何工作的? 只是结合 C 源并开始组装的手动优化组装?

(I cloned the repo and tweaked the.c and.S to compile better with clang -Oz: 992 bytes, down from the original 1208 with gcc. See the WIP-clang-tuning branch in my fork, until I get around to cleaning启动并发送拉取请求. 使用 clang, 系统调用的内联 asm确实节省了整体大小, 特别是一旦 main 没有调用也没有 rets. 如果我想在从编译器 output 重新生成后手动打高尔夫球整个.asm ; 那里肯定是其中的一部分,可以显着节省,例如在循环中使用lodsb 。)


在调用这些标签中的任何一个之前,他们似乎需要r90 ,或者使用寄存器 global var 或者gcc -ffixed-r9来告诉 GCC 永远不要干涉该寄存器 否则 GCC 会在r9中留下任何垃圾,就像其他寄存器一样。

他们的函数是用普通原型声明的,而不是用 6 个 args 和0虚拟参数来让每个调用站点实际上为零r9 ,所以这不是他们的做法。


编码系统调用的特殊方式

我不会将其描述为“编码系统调用”。 也许“定义系统调用包装函数”。 他们正在为每个系统调用定义自己的包装器 function,以一种优化的方式进入底部的一个通用处理程序。 在 C 编译器的 asm output 中,您仍然会看到call write

(对于最终的二进制文件来说,使用 inline asm 让编译器在正确的寄存器中使用 args 内联syscall指令可能会更紧凑,而不是让它看起来像一个普通的 function 来破坏所有调用破坏的寄存器。尤其是如果使用 clang -Oz编译,它将使用 3 字节的push 2 / pop rax而不是 5 字节的mov eax, 2来设置索书号。push push imm8 / pop / syscallcall rel32的大小相同。)


是的,您可以使用.global foo / foo:在手写 asm 中定义函数。 您可以将其视为一个大型 function ,具有用于不同系统调用的多个入口点。 在 asm 中,无论标签如何,执行总是传递到下一条指令,除非您使用 jump/call/ret 指令。 CPU 不知道标签。

所以它就像一个没有中断的 C switch(){}语句break; case:标签之间,或者像 C 标签,您可以使用goto跳转到。 当然,除了在 asm 中,您可以在全局 scope 中执行此操作,而在 C 中,您只能在 function 中执行此操作。 在 asm 中,您可以call而不是goto ( jmp )。

    static long callnum = 0;     // r9 = 0  before a call to any of these

    ...
    socket:
       callnum += 38;
    close:
       callnum++;         // can use inc instead of add 1
    open:                 // missed optimization in their asm
       callnum++;
    write:
       callnum++;
    read:
       tmp=callnum;
       callnum=0;
       retval = syscall(tmp, args);

或者,如果您将其重铸为尾调用链,我们甚至可以省略jmp foo而只是失败:如果您有足够聪明的编译器,像这样的 C 确实可以编译为手写 asm。 (你可以解决 arg-type

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 System V 的最后一个参数传递寄存器,但他们没有使用任何需要 6 个参数的系统调用。 setsockopt需要 5 个参数,因此他们无法跳过mov r10, rcx 但是他们能够将 r9 用于其他事情,而不是需要它来传递第 6 个参数。


有趣的是,他们如此努力地以牺牲性能为代价来节省字节,但仍然使用xor rbp,rbp而不是xor ebp,ebp 除非他们使用gcc -Wa,-Os start.S构建,否则 GAS 不会为您优化 REX 前缀。 GCC 是否优化汇编源文件?

他们可以用xchg rax, r9 (包括 REX 的 2 个字节)而不是mov rax, r9 (REX + opcode + modrm)保存另一个字节。 代码 golf.SE 提示 x86 机器代码

我也使用过xchg eax, r9d因为我知道 Linux 系统调用号适合 32 位,尽管它不会节省代码大小,因为仍然需要 REX 前缀来编码r9d寄存器号。 此外,在它们只需要加 1 的情况下, inc r9d仅为 3 个字节,而add r9d, 1为 4 个字节(REX + opcode + modrm + imm8)。 inc的 no-modrm 短格式编码仅在 32 位模式下可用;在 64 位模式下,它被重新用作 REX 前缀。)

mov rsi,rsp也可以将一个字节保存为push rsp / pop rsi (每个 1 个字节),而不是 3 字节的 REX + mov。 这将为在call exit之前使用xchg edi, eax返回 main 的返回值腾出空间。

但是由于他们没有使用 libc,他们可以内联该exit ,或者将系统调用放在_start下面,这样他们就可以陷入其中,因为exit恰好是编号最高的系统调用! 或者至少jmp exit因为他们不需要堆栈 alignment,并且jmp rel8call rel32更紧凑。


另外,单独的 httpd.asm 自定义二进制文件是如何工作的? 只是结合 C 源并开始组装的手动优化组装?

不,这是完全独立的包含 start.S 代码( ?_017: label 处),并且可能是手动调整的编译器 output。 也许来自链接可执行文件的手动调整反汇编,因此即使对于来自手写 asm 的部分也没有很好的 label 名称。 (具体来说,来自Agner Fog 的objconv ,它在其 NASM 语法反汇编中使用该格式作为标签。)

(Ruslan 还在cmp之后指出了jnz之类的东西,而不是jne对人类具有更合适的语义含义,因此另一个迹象表明它是编译器 output,而不是手写。)

我不知道他们如何安排让编译器不要碰r9 似乎只是运气。 自述文件表明只需编译 .c 和 .S 就可以使用它们的 GCC 版本。

至于 ELF 标头,请参阅文件顶部的注释,该注释链接了 A Whirlwind Tutorial on Creating Seriously Teensy ELF Executables for Linux - 你可以使用nasm -fbin和 output 组装它是一个完整的 ELF 二进制文件,跑步。 不需要链接+剥离,因此您可以考虑文件中的每个字节。

你对正在发生的事情非常正确。 非常有趣,我以前从未见过这样的东西。 但基本上如你所说,每次调用 label 时,如你所说, r9不断累加,直到达到read ,其系统调用号为 0。这就是该命令非常聪明的原因。 假设在调用read之前r9为 0(在调用正确的系统调用之前, read label 本身将r9归零),不需要添加,因为r9已经具有所需的正确系统调用号。 write的系统调用号为1,所以只需要从0加1即可,如宏调用所示。 open的系统调用号是 2,所以首先在open label 时将其加 1,然后在write label 时再次加 1,然后在read ZD304BA20E96D5E34Z11 时将正确的系统调用号放入rax 等等。 rdirsirdx等参数寄存器也没有被触及,所以它基本上就像一个普通的 function 调用。

另外,单独的 httpd.asm 自定义二进制文件是如何工作的? 只是结合 C 源并开始组装的手动优化组装?

我假设你在谈论这个文件 不确定这里到底发生了什么,但看起来像是手动创建了一个 ELF 文件,可能是为了进一步减小大小。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM