This is, of course, highly system-dependent, but since your observations suit a typical Linux/GNU system, I'll refer to such a system.
what I assume is that the linker isn't putting my segments where I think it is.
True, the linker puts the segments not in the order they appear in your code snippet, but rather .text
first, .data
second. We can see this e. g. with
> objdump -h ARR
ARR: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000042 08048074 08048074 00000074 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00007eae 080490b8 080490b8 000000b8 2**2
CONTENTS, ALLOC, LOAD, DATA
compiled programs are able to access non-zero initialized data to the left of (that is, at lower addresses than) the beginning of the array.
Why is this the case …
As we also see in the above example, the .data
section is linked at memory address 080490b8
. Although memory pages have the length PAGE_SIZE (here getconf PAGE_SIZE
yields 4096
, i. e. 100016) and start at multiples of that size, the data starts at an address offset equal to the file offset 000000b8
(where the data is stored in the disk file), because the file pages containing the .data
section are mapped into memory as copy-on-write pages. The non-zero initialized data below the .data
section is just what happens to be in the first file page at bytes 0 to b716, including .text
.
… is there anything I can include in the assembly code that would prevent it?
I'd prefer a solution that places my segments such that a bad array access causes a segfault.
As Margaret Bloom and Ped7g hinted at, you could allocate additional data below ARR
and create an inaccessible guard page. This can be achieved with minimal effort by aligning ARR
to the next page address. The example program below implements this and allows to test it by accepting an index argument (optionally negative) with which the ARR
data is accessed; if within bounds, it should exit with status 0, otherwise segfault. Note: This method works only if the .text
section does not end at a page boundary, because if it does, the .align 4096
is without effect; but since the assembly code is created with a converter program, that program should be able to check this and add a few extra .text
bytes if needed.
.data
.align 4096
ARR:
.space 30000 # we'll actually get 32768
.text
.globl _start
.type _start, @function
_start:
mov (%esp),%ebx # argc
cmp $1,%ebx
jbe 9f
mov $0,%ax
mov $1,%ebx # sign 1
mov 8(%esp),%esi # argv[1]
0: movb (%esi),%cl # convert argument string to integer
jcxz 1f
sub $'0',%cl
js 2f
mov $10,%dx
mul %dx
add %cx,%ax
jmp 3f
2: neg %ebx # change sign
3: add $1,%esi
jmp 0b
1: mul %ebx # multiply with sign 1 or -1
movzx ARR(%eax),%ebx# load ARR[atoi(argv[1])]
9: mov $1,%eax
int $128 # _exit(ebx);