How to link a gas assembly program that uses the C standard library with ld without using gcc?

There are at least three things that you need to do to successfully use libc with dynamic linking:

  1. Link /usr/lib/crt1.o, which contains _start, which will be the entry point for the ELF binary;
  2. Link /usr/lib/crti.o (before libc) and /usr/lib/crtn.o (after), which provide some initialisation and finalisation code;
  3. Tell the linker that the binary will use the dynamic linker, /lib/ld-linux.so.

For example:

$ cat hello.s
 .text
 .globl main
main:
 push %ebp
 mov %esp, %ebp
 pushl $hw_str
 call puts
 add $4, %esp
 xor %eax, %eax
 leave
 ret

 .data
hw_str:
 .asciz "Hello world!"

$ as -o hello.o hello.s
$ ld -o hello -dynamic-linker /lib/ld-linux.so.2 /usr/lib/crt1.o /usr/lib/crti.o -lc hello.o /usr/lib/crtn.o
$ ./hello
Hello world!
$

If you define main in assembly

Matthew's answer does a great job of telling you the minimum requirements.

Let me show you how how to find those paths in your system. Run:

gcc -v hello_world.c |& grep 'collect2' | tr ' ' '\n'

and then pick up the files Matthew mentioned.

gcc -v gives you the exact linker command GCC uses.

collect2 is the internal executable GCC uses as a linker front-end, which has a similar interface to ld.

In Ubuntu 14.04 64-bit (GCC 4.8), I ended up with:

ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 \
  /usr/lib/x86_64-linux-gnu/crt1.o \
  /usr/lib/x86_64-linux-gnu/crti.o \
  -lc hello_world.o \
  /usr/lib/x86_64-linux-gnu/crtn.o

You might also need -lgcc and -lgcc_s. See also: Do I really need libgcc?

If you define _start in assembly

If I defined the _start, the hello world from glibc worked with just:

ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 -lc hello_world.o

I'm not sure if this is robust, i.e. if the crt initializations can be safely skipped to invoke glibc functions. See also: Why does an assembly program only work when linked with crt1.o crti.o and crtn.o?