Why Data and Stack Segments Are Executable

Why data and stack segments are executable?

On modern Linux systems, the linker will mark stack/data non-executable IFF all objects that participate in the link have a special "marker" section .note.GNU-stack.

If you compile e.g. int foo() { return 1; } into assembly (with gcc -S foo.c), you'll see this:

    .section    .note.GNU-stack,"",@progbits

For nasm, the syntax is shown in section 8.9.2 of the manual; you want something like this:

 section .note.GNU-stack noalloc noexec nowrite progbits


Note

This has to be done for every .o file that goes into the executable. If any object file needs executable stack or data, then it's set for the entire segment.

why data segment of c was separated as two sections?

The C programming language (it is a specification written in English) does not know about .bss or .data or data segments. Check that by reading n1570 (the latest draft of C11).

In some cases (e.g. embedded computing) you don't have any data segment. For example when you cross-compile for an Arduino, the resulting code gets uploaded to the flash memory of that microcontroller (and data is in RAM, which your program would perhaps explicitly clear).

(most of my answer below is focused on Linux, but you could adapt it to other OSes)

Regarding the data segment on Unix-like systems like Linux, read more the specification of ELF. It is convenient to avoid spending file space in the executable file for something (the .bss) which is all zeros. The kernel code for execve(2) is able to set up an initial virtual address space with some cleared data (which is not mapped to some file segment). See also mmap(2) & elf(5) & ld-linux(8). Try some cat /proc/$$/maps command to understand the virtual address space of your shell. See proc(5).

So the executable contains segments (some of which are all zeros and don't take any disk space), and is constructed by the linker -which also handle relocations- from several object files provided by the compiler from source code files. On Linux, use objdump(1) and readelf(1) (and nm(1)...) to explore and inspect ELF executables and ELF object files.

BTW a cleared data segment don't need to be fetched from disk by the virtual memory subsystem, and that might make things slightly faster. The kernel would just clear a page in RAM when it is paged in.

So the .bss exist to avoid wasting disk space in executables (and to speedup initializing of zeroed data). Obviously, some mutable data is explicitly initialized to non-zero content (and that needs to sit in .data and takes some disk space in the executable). Constant immutable read-only data goes into .rodata (into the text segment, generally shared by several processes running the same program)

You could configure your linker (e.g. with some linker script) to put all the data (even the cleared ones) in some explicit data segment (but doing so is just wasting disk space)...... Historically, Unix have been developed on machines with little but costly disk space (so wasting it was unthinkable at that time, hence the need of .bss; today you care less!)

Read Levine's book Linkers and Loaders for more, and Advanced Linux Programming and Operating Systems : Three Easy Pieces.

Why is there only one segment in .COM programs, both for the program code and for the stack?

While MS-DOS does set all the segment registers to the same segment on program entry after loading a COM file, there's nothing stopping you from changing the registers to use different segments. However the hard separation between segments you're envisioning doesn't really exist. Even with a separate stack segment, if you overflow your stack you'll still be writing to memory somewhere you shouldn't be and the results will be unpredictable.

Using your second example of CS:IP being 114A:0100 and SS:SP being 114A:FFFE, your COM program could change SS:SP to 210A:03FE. Both SS:SP values would point to the same linear address (2149E) and the same return value that MS-DOS pushed on the stack on entry. You would now only have 1024 bytes for the stack, like in your in EXE example, but your stack segment is still 64k. In real mode, segments are always 64k.

Now guess what happens when SS:SP is 210A:0000 and you try to push a word onto the stack? The CPU doesn't generate an exception, it doesn't somehow refuse to perform the operation, it just does what it always does. It subtracts 2 from SP and then stores the word at the new SS:SP address. In this case that means SP will wrap around to FFFE and the value you push will be stored at 210A:FFFE or at a linear address of 3109E.

Now the question is what is allocated at linear address 3109E? Chances are that MS-DOS allocated this memory to your program and writing values there is harmless since your program isn't using it for anything. However there's a chance that some TSR or driver has allocated the memory there and overwriting it will cause a crash or other unpredictable behaviour.

It would actually be better if you overwrote your own code when the stack overflowed as this would mean you'd be much more likely to notice the bug and fix it.

The same wrap around problem also exists with EXE's. If you want a truly isolated stack you'd need to make 64K in size, but then what happens on overflow is that the stack overwrites itself so doesn't fundamentally eliminate the problem. Ultimately it's your responsibility as a developer to ensure that your program has allocated itself enough stack space and that your code never exceeds that limit.


Note that there's one stack related gotcha you need to worry about with COM files, and that is there's no guarantee that MS-DOS will be able to allocate a full 64K for your COM file. MS-DOS will allocate the biggest free chunk of memory that your COM file will fit into. If that chunk of memory is less than 64K then SP won't be set to FFFE, instead it will set SP to point to the last word allocated to your program. In the worst case that means the stack will immediately start overwriting the code and data in your COM file. For that reason it's a good idea to allocate space at the end of your COM file to reserve space for the stack.

How is the .data segment loaded into a seperate memory area than the .code segment in x86?

It's up to the bootloader to handle this so it ultimately depends on what the bootloader does, but generally bootloaders are loading standard executable images in standard formats like PECOFF or ELF. They load the kernel much like operating systems load program executables.

Sections exist so that the contents of sections with the same name will all be grouped together contiguously in the executable. The linker will take the contents of all the .text sections in all the input object files and combine them into one .text section in the output executable. Similarly it will do the same for .data and other named sections.

The linker takes all these combined sections and places them one after each other in the executable, creating one contiguous image that can be loaded into memory. Under an operating system the executable would be loaded into memory in one single contiguous chunk. If necessary, relocations would be applied to account for the executable being loaded at a different address than where it was intended to be loaded and uninitialized data segments (.bss) would be initialized with zeroes. Finally the permissions of each page of the executable in memory would be adjusted according to the segments they belong to. For example, pages in .text sections would be marked read-only and executable, while in .data sections they would be marked read/write and not executable.

(Note that this simplifies and glosses over many details how linkers and operating systems create and load executables. It's possible for sections to be merged, renamed, discarded, etc. Padding space may be inserted between segments so that they're page aligned. Under ELF named sections in object files are actually converted unnamed program segments in executables and it's these program segments that determine page permissions.)

A bootloader loads a kernel executable much like an operating system, but may not support relocations, and won't change page permissions because those need an operating system to work. The kernel itself is responsible setting up its own page permissions.

So the kernel code and data gets loaded into one single contiguous area of memory, with that area of memory subdivided into separate areas for the code, data and any other sections the kernel uses.

Section vs. segment?

In the context of ELF, they are two different related things.

  • Segments are described in the program header. Loosely, each segment describes a chunk of the file to be loaded into memory when an executable is run.

  • Sections are described in the section header. Loosely, each section describes a chunk of data relevant to the program.

So both sections and segments are chunks of the file, described as an offset and size (though in both cases the size may be 0, in which case the offset is ignored). Any given ELF file might have only segments, or only sections, or both segments and sections. In order to be executable it must have segments to load. In order to be linkable, it must have sections describing what is where.

Dynamically linked executable must have segments, but sections are still optional: There is a PT_DYNAMIC segment (see this) which indicates the content of the .dynamic section. In this way, the dynamic linker can still find the offset of the symbol table .dynsym.

In general, segments do not overlap each other and sections do not overlap each other, but sections may describe data that is part (or all) of a segment. That is not a strict requirement of the format, but it would be strange to violate. It would also be very strange for a section to describe data in two different segments. There are also (generally) sections that are not part of any segment.

Where are the stack, data and instruction segments implemented?

You need to use a different model to think of memory usage. Think of memory using a program sections with attributes like:

  • Code: Executable, readable, no write
  • Static Data: Nonexecutable, readable, nowrite
  • Modifiable Data: Nonexecutable, readable, writeable
    (You might also add Demand-zero areas)

These attributes can be set at the page level. In a system you could have the pages interleaved

Code-Data-Code-Data-Code-Data

Normally, linkers will put similar sections together but it can lay out the memory in nearly any way with paging. The usual mechanism is that if you give names to sections, linkers will collect and group things with the same section together.

You are no restricted by the segmentation system from Ye Olde 8086.

The parts are then governed by the operating system and the linker.

Linux default behavior of executable .data section changed between 5.4 and 5.9?

This is only a guess: I think the culprit is the READ_IMPLIES_EXEC personality that was being set automatically in the absence of a PT_GNU_STACK segment.

In the 5.4 kernel source we can find this piece of code:

SET_PERSONALITY2(loc->elf_ex, &arch_state);
if (elf_read_implies_exec(loc->elf_ex, executable_stack))
current->personality |= READ_IMPLIES_EXEC;

That's the only thing that can transform an RW section into an RWX one. Any other use of PROC_EXEC didn't seem to be changed or relevant to this question, to me.

The executable_stack is set here:

for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++)
switch (elf_ppnt->p_type) {
case PT_GNU_STACK:
if (elf_ppnt->p_flags & PF_X)
executable_stack = EXSTACK_ENABLE_X;
else
executable_stack = EXSTACK_DISABLE_X;
break;

But if the PT_GNU_STACK segment is not present, that variable retains its default value:

int executable_stack = EXSTACK_DEFAULT;

Now this workflow is identical in both 5.4 and the latest kernel source, what changed is the definition of elf_read_implies_exec:

Linux 5.4:

/*
* An executable for which elf_read_implies_exec() returns TRUE will
* have the READ_IMPLIES_EXEC personality flag set automatically.
*/
#define elf_read_implies_exec(ex, executable_stack) \
(executable_stack != EXSTACK_DISABLE_X)

Latest Linux:

/*
* An executable for which elf_read_implies_exec() returns TRUE will
* have the READ_IMPLIES_EXEC personality flag set automatically.
*
* The decision process for determining the results are:
*
* CPU: | lacks NX* | has NX, ia32 | has NX, x86_64 |
* ELF: | | | |
* ---------------------|------------|------------------|----------------|
* missing PT_GNU_STACK | exec-all | exec-all | exec-none |
* PT_GNU_STACK == RWX | exec-stack | exec-stack | exec-stack |
* PT_GNU_STACK == RW | exec-none | exec-none | exec-none |
*
* exec-all : all PROT_READ user mappings are executable, except when
* backed by files on a noexec-filesystem.
* exec-none : only PROT_EXEC user mappings are executable.
* exec-stack: only the stack and PROT_EXEC user mappings are executable.
*
* *this column has no architectural effect: NX markings are ignored by
* hardware, but may have behavioral effects when "wants X" collides with
* "cannot be X" constraints in memory permission flags, as in
* https://lkml.kernel.org/r/20190418055759.GA3155@mellanox.com
*
*/
#define elf_read_implies_exec(ex, executable_stack) \
(mmap_is_ia32() && executable_stack == EXSTACK_DEFAULT)

Note how in the 5.4 version the elf_read_implies_exec returned a true value if the stack was not explicitly marked as not executable (via the PT_GNU_STACK segment).

In the latest source, the check is now more defensive: the elf_read_implies_exec is true only on 32-bit executable, in the case where no PT_GNU_STACK segment was found in the ELF binary.

I assembled your program, linked it, and found no PT_GNU_STACK segment, so this may be the reason.

If this is indeed the issue and if I followed the code correctly, if you set the stack as not executable in the binary, its data section should not be mapped executable anymore (not even on Linux 5.4).



Related Topics



Leave a reply



Submit