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.