0

How does C compiled to ARM ASM know where to branch to for external functions?

For example, here is a simple C program:

#include <stdio.h>

int main() {
   printf("Hello World!");
   return 0;
}

and its corresponding ARM ASM program:

    .arch armv6
    .eabi_attribute 28, 1
    .eabi_attribute 20, 1
    .eabi_attribute 21, 1
    .eabi_attribute 23, 3
    .eabi_attribute 24, 1
    .eabi_attribute 25, 1
    .eabi_attribute 26, 2
    .eabi_attribute 30, 6
    .eabi_attribute 34, 1
    .eabi_attribute 18, 4
    .file   "main.c"
    .text
    .section    .rodata
    .align  2
.LC0:
    .ascii  "Hello World!\000"
    .text
    .align  2
    .global main
    .arch armv6
    .syntax unified
    .arm
    .fpu vfp
    .type   main, %function
main:
    @ args = 0, pretend = 0, frame = 0
    @ frame_needed = 1, uses_anonymous_args = 0
    push    {fp, lr}
    add fp, sp, #4
    ldr r0, .L3
    bl  printf
    mov r3, #0
    mov r0, r3
    pop {fp, pc}
.L4:
    .align  2
.L3:
    .word   .LC0
    .size   main, .-main
    .ident  "GCC: (Raspbian 10.2.1-6+rpi1) 10.2.1 20210110"
    .section    .note.GNU-stack,"",%progbits

I dont see a "printf" tag anywhere so i am assuming that it links outside of the program. but how does it know where to search? it wouldnt look everywhere, because there might be duplicate tags, but there are also libraries that are placed (in the computers perspective) at random, though i also dont see anywhere where it defines a library location.

so where does it link, for more than just the standard C library?
and how can i compile it to not rely on those external dependencies?
or know where the libraries are so i know which files i can delete?

i am currently operating linux on a raspberry pi 400

ShortsKing
  • 47
  • 1
  • 8

1 Answers1

2

My computer has an x86_64 processor, but the principle is the same. I'm using gcc 9.3.0.

I copied your code into a file called main.c and compiled it to assembly with gcc -S main.c. It produced the file main.s with the following contents:

        .file   "main.c"
        .text
        .section        .rodata
.LC0:
        .string "Hello World!"
        .text
        .globl  main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
        endbr64
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16 
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        leaq    .LC0(%rip), %rdi
        movl    $0, %eax
        call    printf@PLT
        movl    $0, %eax
        popq    %rbp
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:
        .size   main, .-main
        .ident  "GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0"
        .section        .note.GNU-stack,"",@progbits
        .section        .note.gnu.property,"a"
        .align 8
        .long    1f - 0f
        .long    4f - 1f
        .long    5   
0:
        .string  "GNU"
1:
        .align 8
        .long    0xc0000002
        .long    3f - 2f
2:
        .long    0x3 
3:
        .align 8
4:

There are a lot of assembler directives here that can make it confusing to read, so I assembled it into an object file (gcc -c main.s) and then ran objdump -d main.o to disassemble it. Here is the output of the disassembly:

main.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
   0:   f3 0f 1e fa             endbr64 
   4:   55                      push   %rbp
   5:   48 89 e5                mov    %rsp,%rbp
   8:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi        # f <main+0xf>
   f:   b8 00 00 00 00          mov    $0x0,%eax
  14:   e8 00 00 00 00          callq  19 <main+0x19>
  19:   b8 00 00 00 00          mov    $0x0,%eax
  1e:   5d                      pop    %rbp
  1f:   c3                      retq   

The first three instructions here are boilerplate, so we'll ignore them. The first interesting instruction is

lea    0x0(%rip),%rdi

This is meant to load the address of the "Hello World!" string into register %rdi. Confusingly, it appears to simply be copying %rip into %rdi.

The next instruction puts a 0 into register %eax. I actually don't know why this is, but it's not really relevant to this discussion.

Then comes the actual call to printf:

callq  19 <main+0x19>

Once again, this uses an address that doesn't seem correct. You may notice that address 0x19 actually points to the next instruction.

The next 3 instructions basically perform the final return 0.

To really answer your question we need to look at more than just assembly code. At this point I would recommend taking some time to research the format of ELF files. I would consider that topic to be beyond the scope of this answer, but it will help you understand what I'm about to show you.

I first want to point out that in both your assembly and mine, the "Hello World!" string is preceded by this directive:

.section        .rodata

whereas the main function is preceded by

.text

which is shorthand for

.section        .text

These directives instruct the assembler on how to arrange the code and data in the object file. You can see this by printing the section headers of the object file:

$ readelf -S main.o
There are 14 section headers, starting at offset 0x318:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       0000000000000020  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  00000258
       0000000000000030  0000000000000018   I      11     1     8
  [ 3] .data             PROGBITS         0000000000000000  00000060
       0000000000000000  0000000000000000  WA       0     0     1
  [ 4] .bss              NOBITS           0000000000000000  00000060
       0000000000000000  0000000000000000  WA       0     0     1
  [ 5] .rodata           PROGBITS         0000000000000000  00000060
       000000000000000d  0000000000000000   A       0     0     1
  [ 6] .comment          PROGBITS         0000000000000000  0000006d
       000000000000002b  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  00000098
       0000000000000000  0000000000000000           0     0     1
  [ 8] .note.gnu.propert NOTE             0000000000000000  00000098
       0000000000000020  0000000000000000   A       0     0     8
  [ 9] .eh_frame         PROGBITS         0000000000000000  000000b8
       0000000000000038  0000000000000000   A       0     0     8
  [10] .rela.eh_frame    RELA             0000000000000000  00000288
       0000000000000018  0000000000000018   I      11     9     8
  [11] .symtab           SYMTAB           0000000000000000  000000f0
       0000000000000138  0000000000000018          12    10     8
  [12] .strtab           STRTAB           0000000000000000  00000228
       000000000000002a  0000000000000000           0     0     1
  [13] .shstrtab         STRTAB           0000000000000000  000002a0
       0000000000000074  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)

If you can figure out how to read this output, you will see that the .text section is 0x20 bytes in size (which matches the above disassembly output), and the .rodata section is 0xd (13) bytes in size (i.e. strlen("Hello World!") plus a null byte). The answer your question, however, is in the relocation data:

$ readelf -r main.o

Relocation section '.rela.text' at offset 0x258 contains 2 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
00000000000b  000500000002 R_X86_64_PC32     0000000000000000 .rodata - 4
000000000015  000c00000004 R_X86_64_PLT32    0000000000000000 printf - 4

Relocation section '.rela.eh_frame' at offset 0x288 contains 1 entry:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000020  000200000002 R_X86_64_PC32     0000000000000000 .text + 0

This output is also very confusing to read if you don't know what it means. The first thing to understand is that the relocation sections tell the linker about places in the code that depend on symbols, either in other sections of the same file, or, more frequently, symbols that are defined in other files. The .rela.text section, for example, contains relocation information about the .text section. When this object file is linked into the final executable, the linker will overwrite part of the .text section with the missing addresses.

So, looking at the first entry under .rela.text, we see an offset of 0xb. Looking at the disassembly, we can see that offset 0xb references the fourth byte of the lea instruction's 7-byte encoding. The type, R_X86_64_PC32, tells us that that instruction is expecting a 32-bit PC-relative address, so we can expect the linker to overwrite the next 4 bytes (currently all 0). The rightmost column tells us, in human readable format, that this address needs to be populated with the address of the .rodata section minus 4 (with PC-relative addressing you have to remember that the PC will be pointing at the next instruction). It leaves out the fact, implicit for relocation type R_X86_64_PC32, that it will then subtract from that the final address of byte 0xb in the .text section, which will make that a valid PC-relative pointer to the "Hello World!" string data.

Similarly, the second entry tells the linker to copy the address of printf (minus 4) to offset 0x15 in the .text section, which would be the last 4 bytes of the callq instruction encoding. In this case, the type is R_X86_64_PLT32, which tells us that it's pointing to an entry in the procedure linkage table (PLT). A PLT is used for dynamic linking so that shared object libraries (in this case libc.so) can be loaded into physical memory once and shared by many running executables.

As a note, that might answer some of your specific questions, your compiler automatically links all the runtime libraries needed to execute a program. This includes any standard library functions, which would be part of libc.so. The only way to run without "external dependencies" would be to run on a bare-metal system (i.e. one without an operating system). Any operating system you use will have to do some amount of work to get your program to the start of main().

TallChuck
  • 1,725
  • 11
  • 28
  • As for why `eax` is set to zero before calling `printf`, refer to https://stackoverflow.com/questions/6212665/why-is-eax-zeroed-before-a-call-to-printf – ecm Feb 28 '22 at 18:55
  • You can disassemble with `objdump -drwC` to print relocation info as comments on each line, so the `call rel32=0` would actually show a meaningful target name. And same for the RIP-relative LEA of an address in `.rodata`, via the non-global symbol `.LC0`. – Peter Cordes Mar 01 '22 at 03:24
  • re: looking at compiler output in general, [How to remove "noise" from GCC/clang assembly output?](https://stackoverflow.com/q/38552116) is helpful to filter directives. But maybe not helpful when you don't already know what to assume about which sections code vs. data will be in. – Peter Cordes Mar 01 '22 at 03:26
  • *The only way to run without "external dependencies" would be to run on a bare-metal system* - If you make system calls directly from inline asm, you can avoid library dependencies, including writing your own `_start` to not even link the CRT startup code. e.g. `gcc -ffreestanding -nostdlib -static foo.c -o my_static_executable` can make a binary similar to what you could write by hand in a `.S` file. See also [How Get arguments value using inline assembly in C without Glibc?](https://stackoverflow.com/q/50260855) - a `_start` written in C can be even simpler if you don't want argc,argv. – Peter Cordes Mar 01 '22 at 03:29