gccassemblyldelfbfd

Why can't I pipe assembler output to stdout?


[edit]
This was just kind of a though experiment I had where I wanted to see if I could trick the kernel in to executing an elf from an unnamed pipe with process substitution with /lib64/ld-linux-x86-64.so.2, I knew it was a shot in the dark but I was just hoping to see if anyone could give me an answer as to why it didn't work

$ /lib64/ld-linux-x86-64.so.2 <(gcc -c -xc <(echo $'#include <stdio.h>\n\nint main(){\nprintf("I work\\n");\nreturn 0;\n}') -o /dev/stdout)
/tmp/ccf5sMql.s: Assembler messages:
/tmp/ccf5sMql.s: Fatal error: can't write /dev/stdout: Illegal seek
as: BFD version 2.25.1-22.base.el7  assertion fail elf.c:2660
as: BFD version 2.25.1-22.base.el7  assertion fail elf.c:2660
/tmp/ccf5sMql.s: Fatal error: can't close /dev/stdout: Illegal seek
/dev/fd/63: error while loading shared libraries: /dev/fd/63: file too short

I figured that it may have been possible due to varying results I was getting.

$ /lib64/ld-linux-x86-64.so.2 <(gcc -fPIC -pie -xc <(echo $'#include 
<stdio.h>\n\nint main(){\nprintf("I work\\n");\nreturn 0;\n}') -o 
/dev/stdout|cat|perl -ne 'chomp;printf')
/dev/fd/63: error while loading shared libraries: /dev/fd/63: ELF load 
command past end of file
$ /lib64/ld-linux-x86-64.so.2 <(gcc -fPIC -pie -xc <(echo $'#include 
<stdio.h>\n\nint main(){\nprintf("I work\\n");\nreturn 0;\n}') -o 
/dev/stdout|cat|perl -0 -ne 'chomp;printf')
/dev/fd/63: error while loading shared libraries: /dev/fd/63: ELF file ABI 
version invalid

So I was playing around with ASM and noticed that you can't assemble or link output to stdout.

$ as /tmp/lol.s -o /dev/stdout
/tmp/lol.s: Assembler messages:
/tmp/lol.s: Fatal error: can't write /dev/stdout: Illegal seek
as: BFD version 2.25.1-22.base.el7  assertion fail elf.c:2660
as: BFD version 2.25.1-22.base.el7  assertion fail elf.c:2660


as /tmp/lol.s -o /tmp/test.o
$ ld /tmp/test.o -o what -lc
ld: warning: cannot find entry symbol _start; defaulting to 00000000004002a0


$ exec 9< <(ld /tmp/test.o -o /dev/stdout -lc)
ld: warning: cannot find entry symbol _start; defaulting to 00000000004002a0
ld: final link failed: Illegal seek

Given the code as follows:

.file   "63"
.section        .rodata
.LC0:
.string "I work"
.text
.globl  main
.type   main, @function
main:
.LFB0:
.cfi_startproc
pushq   %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq    %rsp, %rbp
.cfi_def_cfa_register 6
movl    $.LC0, %edi
call    puts
movl    $0, %eax
popq    %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size   main, .-main
.ident  "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-16)"
.section        .note.GNU-stack,"",@progbits
.file   "63"
.section        .rodata

Can anyone tell me why it isn't possible to assemble objects or link objects to stdout? Please be as in depth as possible. To see the full process that a compiler goes to in order to generate that code, you can use the following:

$ exec 7< <(gcc -c -xc <(echo $'#include <stdio.h>\n\nint main(){\nprintf("I work\\n");\nreturn 0;\n}') -o /dev/stdout)

If you assemble and link the assembly I provided earlier and want to execute it properly you'll need to call /lib64/ld-linux-x86-64.so.2 /path/to/output otherwise it will just say bad elf interpreter.

# ./what
bash: ./what: /lib/ld64.so.1: bad ELF interpreter: No such file or directory

# /lib64/ld-linux-x86-64.so.2 ./what
I work

Solution

  • You can't pipe assembler output to stdout, because since immemorial times (1960s probably) assemblers work generally in two passes (and not only on input, but also on output). So ability to seek (both input and output, using lseek(2)) is required. Otherwise they would need to keep most of the input and output data in memory.

    Remember that an object file contains not only data (e.g. machine instructions, read only constant) but also relocation information.

    /tmp/lol.s: Fatal error: can't write /dev/stdout: Illegal seek

    This illustrates that the as program needs to seek files (e.g. using lseek(2)).

    Perhaps you want to generate machine code in memory. For that use some JIT compilation library like libgccjit or asmjit.

    BTW, you might want to understand how gcc is compiling a simple C program. For that compile it with gcc -v and notice that some crt0 thing is linked.

    If you considered using pipes for performance reasons, use some tmpfs filesystem instead. The files there stay in memory (so are lost at shutdown) and are quick because no disk IO is performed.

    You could even generate some C file in such a file system, then ask gcc to compile it (perhaps as a plugin). See also this.

    ... If I could trick the kernel in to executing an elf from an unnamed pipe

    No, you can't. An ELF executable needs to be seekable too, because the kernel is, at its execve(2) time, setting up​‎​‎​‎​‎ a fresh virtual addresss space, using something close to mmap(2) internally. In other words, execve is setting up several memory mappings.

    Study the virtual address space of your processes. Read proc(5), then try cat /proc/$$/maps (and replace $$ with a more interesting pid).

    Reading Operating Systems: Three Easy Pieces (freely downloadable) should be interesting to you.