Why does subtracting the value of etext from edata not give me the correct size for my text segment

Viewed 49

I have a C source code file like this -

#include <stdio.h>

extern char etext, edata, end;

int main(int argc, char* argv[], char* envp[]) {

    printf("TEXT END: %p\n", &etext);
    printf("DATA END: %p\n", &edata);
    printf("PROG BRK: %p\n", &end);

    register long volatile sp asm("rsp");
    printf("STCK PTR: %p\n", sp);

    return 0;
}

Which gives me this output -

TEXT END: 0x5565d48661d5
DATA END: 0x5565d4869018
PROG BRK: 0x5565d4869020
STCK PTR: 0x7fff8f6f34d0

To my understanding, the text, data, and bss segments are laid out sequentially in memory with no space between them. But (&edata - &etext) is 0x2e43 while size -x a.out says the size of the data section is 0x250. Why does this happen?

I am using gcc 12.2.0 on linux x86_64 5.19.9.

2 Answers

To my understanding, the text, data, and bss segments are laid out sequentially in memory with no space between them.

That used to be true a long time ago. Nowadays it usually isn't the case, for several different reasons.

Also, back when it was true, edata - etext would give you the size of the data segment, not the text segment.

To get a look at the actual layout of the address space on a current-generation Linux-based Unix, you can read /proc/self/maps, and/or you can use dl_iterate_phdr. Unfortunately, both of these APIs are Linux-specific. Below is a demo program that takes the latter approach (note: some of this code assumes a 64-bit address space).

#define _GNU_SOURCE 1
#include <elf.h>
#include <link.h>
#include <stdint.h>
#include <stdio.h>

static void print_flags(const ElfW(Phdr) *p) {
    putchar((p->p_flags & PF_R) ? 'r' : '-');
    putchar((p->p_flags & PF_W) ? 'w' : '-');
    putchar((p->p_flags & PF_X) ? 'x' : '-');

    ElfW(Word) other_flags = p->p_flags & ~(PF_R | PF_W | PF_X);
    if (other_flags) {
        printf("/%08x", other_flags);
    }
}

static int callback(struct dl_phdr_info *dlpi, size_t size, void *data) {
    if (dlpi->dlpi_name[0] == '\0') {
        puts("Main executable:");
    } else {
        printf("%s:\n", dlpi->dlpi_name);
    }
    for (int i = 0; i < dlpi->dlpi_phnum; i++) {
        if (dlpi->dlpi_phdr[i].p_type != PT_LOAD) {
            continue;
        }
        uint64_t start =
            (uint64_t)(dlpi->dlpi_addr + dlpi->dlpi_phdr[i].p_vaddr);
        uint64_t end = start + dlpi->dlpi_phdr[i].p_memsz;
        printf("  [%016lx, %016lx) ", start, end);
        print_flags(&dlpi->dlpi_phdr[i]);
        putchar('\n');
    }
    return 0;
}

int main(void) {
    dl_iterate_phdr(callback, 0);
    return 0;
}

Typical output will be something like this:

$ ./a.out
Main executable:
  [000056371bce2000, 000056371bce2730) r--
  [000056371bce3000, 000056371bce3315) r-x
  [000056371bce4000, 000056371bce4168) r--
  [000056371bce5da0, 000056371bce6020) rw-
linux-vdso.so.1:
  [00007ffe1ff9c000, 00007ffe1ff9cdcd) r-x
/lib/x86_64-linux-gnu/libc.so.6:
  [00007f1c05caf000, 00007f1c05cd6fe0) r--
  [00007f1c05cd7000, 00007f1c05e6b4c1) r-x
  [00007f1c05e6c000, 00007f1c05ec38cc) r--
  [00007f1c05ec48f0, 00007f1c05ed6e50) rw-
/lib64/ld-linux-x86-64.so.2:
  [00007f1c05f0c000, 00007f1c05f0db50) r--
  [00007f1c05f0e000, 00007f1c05f37335) r-x
  [00007f1c05f38000, 00007f1c05f42ee4) r--
  [00007f1c05f44620, 00007f1c05f472d8) rw-

As you can see, for each module, the "segments" printed by this program do not correspond to the traditional 3-way division into text, data, and bss. Approximately speaking, the "r-x" segment holds the text and the "rw-" segment holds the combination of data and bss. The "r--" segments hav no traditional analogue. It may be useful to compare this program's output with the output of readelf --program-headers on the executable and each of its shared libraries.

This program layout test used to work for unix programs in the old days. It still does to some extend on selected systems, such as linux, but the etext, edata and end should be declared as arrays as they are addresses, not variables:

extern char etext[], edata[], end[];

Here is a modified program:

#include <stdio.h>

extern char etext[], edata[], end[];

int main(int argc, char *argv[], char *envp[]) {

    // Note that main is usually not the first item in the text segment
    printf("main    : %p\n", (void *)main);
    printf("TEXT END: %p\n", (void *)etext);
    printf("DATA END: %p\n", (void *)edata);
    printf("PROG BRK: %p\n", (void *)end);

    void *sp = &argc;
    printf("STCK PTR: %p\n", sp);

    return 0;
}

Output on my linux system:

main    : 0x4004b0
TEXT END: 0x40068d
DATA END: 0x600a40
PROG BRK: 0x600a48
STCK PTR: 0x7ffc52b616bc

As you can see, the text and data segments are not contiguous on this system, so the size of the text segment. edata - etext used to evaluate to the size of the data segment, but this no longer works.

Also note that successive runs of the program may output different addresses on modern systems because of address space randomization, a technique that makes it more difficult for hackers to exploit software flaws.

Related