SLS Lecture 14 : Assembly using the OS
Contents
14. SLS Lecture 14 : Assembly using the OS#
Review of Processes
Review of Systems Calls
I/O - Assembly “Hello World” and read
Process Address Spaces
Dynamic Memory : Adding and Growing our heap
create a directory
mkdir syscalls; cd syscalls
copy examples
add a
Makefile
to automate assembling and linkingwe are going run the commands by hand this time to highlight the details
add our
setup.gdb
to make working in gdb easiernormally you would want to track everything in git
14.1. Review of Processes#
14.1.1. Processes and the Kernel#
14.2. Review of System Calls - From Lecture 08#
|
Remember this…
The “Kernel” – Unique to Every OS
Bootstraps the HW and has direct access to all of it
Bottom layer that enables other programs to run
A unique collection of functions that programs can invoke
Not useful on its own only useful and accessed by running other programs.
14.2.1. Intel syscall
#
On Intel the instruction is syscall
– NOTE “CLOBBERS” RCX and R11
14.2.2. The OS System Calls#
Each OS Kernel provides a set of calls that a process can invoke using the syscall
instruction on an Intel based computer
The Linux Kernel supports a very large number of system calls each is identified by a unique number that must be placed in RAX
prior to executing the syscall
instruction. Additional arguments are passed in by setting other registers.
With each version of the Kernel the table of calls changes. Here is one site that provides a list
14.2.3. LINUX SYSTEM CALLS#
reading some man pages
man syscalls
andman syscall
we find thatto exit we must place
60
inrax
and that the value we want to set as our exit status code in
rdi
each system call will accept arguments in various registers
NOTE: On INTEL 64bit processors
rcx
will be overwritten by the syscall instructionOn INTEL 64bit processors Linux system calls will return a value back in
rax
and possiblyrdx
rax
and possiblyrdx
will be overwritten by a system call
.intel_syntax noprefix
.text
.global _start
_start:
mov rax, 60 # Linux exit system call number is 60
mov rdi, 0 # rdi is return value 0 success
syscall
14.3. I/O#
14.3.1. Review from Lecture 3#
14.3.2. Finally “Hello World in Assembly”#
CODE: asm - hello.s
.intel_syntax noprefix
.section .rodata # readonly data
str:
.string "Hello World\n" # the string
.section .text
.global _start
_start:
mov rax, 1 # write syscall number 1
mov rdi, 1 # fd = rdi = stdout = 1
mov rsi, OFFSET str # address of data to send
mov rdx, 12 # length of data
syscall
mov rax, 60 # exit syscall number 60
mov rdi, 3 # rdi = exit status code = 3
syscall
14.3.2.1. At long last we have a program that we don’t need the debugger for!#
14.3.3. How about input – remember standard input#
display(Markdown(FileCodeBox(
file="../src/read.s",
lang="gas",
title="<b>CODE: asm - read.s",
h="100%",
w="107em"
)))
CODE: asm - read.s
.intel_syntax noprefix
.section .data
buffer:
.byte 0x0 # space to read one byte
# from standard input
.section .text
.global _start
_start:
mov rax, 0 # read syscall number 0
mov rdi, 0 # rdi = fd = stdin = 0
# rsi address of memory to place data read
mov rsi, OFFSET buffer
mov rdx, 1 # maximum bytes to read
syscall
# will "block" until data is received on
# standard input -- this means we will
# hang here until some presses enter
# now exit
mov rax, 60
mov rdi, 0
syscall
14.3.4. Blocking: System calls can “block” your program#
By default the read system call will not return until some data is read or an error occurs. So when we run the read example it will wait for the read to return before it goes on to exit. This waiting on a system call is called “blocking”
14.3.5. There are many more calls for doing I/O#
But we can now
transfer bytes from our address space to a “file” - write system call
transfer bytes from a “file” to our address space - read system call
connect (and possibly create) files to our address space for reading and writing - open system call
disconnect a file from our address space - close system call
14.3.6. Exercises#
Change the usesum
program to:
exit properly
allocate memory for the data
read the data in from standard in
write the binary result to standard out
use a shell command like
od -l -A10
to convert the output to ascii
open the binary file of numbers and read them in
can you figure out how to get the command line arguments?
Write a new program that uses the random instruction to create a data file
14.4. Process Address Spaces#
14.4.1. Address Space organizes Memory of a Process#
Remember
The binaries we create using the assembler and linker are Executables
When we “run” our executables via the shell it creates a new process context and our binary is loaded as the initial memory “image”.
The memory and register values of the process are unique to each process and change as the instructions of our binary “execute”
14.4.2. Assembly Programming as Process Address Space Image creation#
14.4.3. The Details#
14.4.4. Process Address Space#
The Memory of a Process is organized as an (Virtual) Address Space
Each possible location of a byte is identified by a unique address (number)
To associate a Region (range of continues addresses) with actual memory requires a “mapping”
mappings associate a Region with Memory and a possible source of values
mappings can restrict what kind of access can be made to region
fetch for execute: x
read: r
write: w
14.5. Summary of how we “loading” a process address from a binary#
14.6. Code and instructions for exploring the Process Address Space layout#
In the lecture notes at this point you will find code and instructions for exploring the address space layout
How we control the mappings based on the sections we add
How we can add and shrink heap memory using the
brk
system call
To do the exploration we use :
The
nm
command that print’s the symbol table of a binary. As such we can use it to see what addresses the linker placed our bytes at.A special file that the OS provides that describes the address space of a running process call the
maps
file.
Detail are provide in the notes. Don’t forget you can also use gdb to probe the various mappings you discover.
Eg. You can use the x
command to examine the bytes at the addresses of the memory areas you find in the maps file.
14.6.1. Exploring the Address Space of a process with Code#
Let’s explore the address space of a running program
We will use 5 versions of a program the progressively adds more regions to the address space
The programs will wait for input before exiting so that we can use the OS provided file for check what the process address space looks like
14.6.1.1. Version 1: text and data#
CODE: asm - exploringASlayout1.s
# To use this assemble and link it:
# as -g exploringASlayout1.s -o exploringASlayout1.o
# ld -g exploringASlayout1.o -o exploringASlayout1
# Then using two terminals:
# 1. use the nm command on the binary to see what addresses the linker
# placed all the symbols at Eg.
# $ nm exploringASlayout1
# 0000000000403000 D __bss_start
# 0000000000403000 D _edata
# 0000000000403000 D _end
# 0000000000402000 d rwdata
# 0000000000401000 T _start
# (if you like you can also run: "readelf -S <binary>" to
# see more information about the sections in the binary)
# 2. then run the binary -- its should stop on the read
# Eg.
# $ ./exploringASlayout1
#
# 3. in another terminal use ps and grep to find the process started from the binary
# Eg.
# $ ps auxgww | grep exploring
# jovyan 1697 0.0 0.0 168 4 pts/29 S+ 13:41 0:00 ./exploringASlayout1
# jovyan 1699 0.0 0.0 6440 720 pts/30 S+ 13:42 0:00 grep exploring
# $
# We see the process id (pid) is 1697
# 4. using the process id you can now examine the file "/proc/<pid>/maps" to
# see the layout of the running processes address space
# Eg.
# $ cat /proc/1697/maps
# 00400000-00401000 r--p 00000000 103:05 1189363 /home/jovyan/exploringASlayout1
# 00401000-00402000 r-xp 00001000 103:05 1189363 /home/jovyan/exploringASlayout1
# 00402000-00403000 rw-p 00002000 103:05 1189363 /home/jovyan/exploringASlayout1
# 7ffd13044000-7ffd13065000 rw-p 00000000 00:00 0 [stack]
# 7ffd13069000-7ffd1306d000 r--p 00000000 00:00 0 [vvar]
# 7ffd1306d000-7ffd1306f000 r-xp 00000000 00:00 0 [vdso]
# ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
# $
# What we see is that there are three mappings to the binary file, a mapping for the stack and then some
# extra's used by the OS.
# 5. you can now press enter in the terminal that the binary is running in
# this will cause the read to finish and the binary will then continue to execute
# CODE to explore how we control the address space mappings
# The smallest size of an address space mapping is called the page size
# on Linux the default is 4096 (4Kb). To make it easier to identify
# the mappings we will fill each section we add to a full page (4Kb)
# worth of bytes.
# This version 1 starts with just two sections
# One page' of "text" and one page of "data"
# - text should get mapped to a region that is readable and executable (r-x)
# - data should get mapped to a region that is readable and writable (rw-)
.intel_syntax noprefix # assembler syntax to use <directive>
.section .text # linker section <directive>
.global _start # linker symbol type <directive>
_start:
# use a read system call to stop the program until user presses enter
# so that we can examine things before and after the code runs
mov rax, 0 # read syscall = 0 -- read(fd, buf, len)
mov rdi, 0 # fd = rdi = 0 = stdin
mov rsi, OFFSET rwdata # buf = rsi = rwdata memory
mov rdx, 1 # len = rdx = 1 wait for one byte "enter"
syscall
mov rax, 60 # exit system call
mov rdi, 0 # exit status 0
syscall
# fill the text section up with zeros so that it is one page in size.
.org 4095 # force the next byte to be at the end of the page (4095
.byte 0x00 # put a byte of zero here
##### DATA SECTION : read and writable memory s
.section .data
.global rwdata
rwdata: .fill 4096,1,0xff
# the following tells the linux kernel to set permissions the way we expect
# rather than the defaults which map all sections as executable
# When using a compiler it will add this for us
.section .note.GNU-stack,"",@progbits
Use the nm command to see where the linker placed things:
Now in one terminal run the binary – start a process from it
./exploringASlayout1
It will hang on the read so that we have time to explore what the process’s address space looks like.
In another terminal use ps and grep to find the process id of the new process
ps auxgww | grep exploring
Now we can use the process id to find the special file that the OS provides for us to examine the address space of a process.
cat /proc/<pid>/maps
You can also use gdb to to explore the regions of memory that the maps file tells us are present. You can also compare what is in memory to what you find in the file.
When you are done you can press return in the terminal that the program in running. This will send a newline to the standard input of the program. This will cause the read to return and the program will go on to exit.
You can find lots of information ab out the OS provided proc
directory in the manual man proc
In the manual page there is a section on the maps
file specifically that tells us the details of what information it presents regarding the mappings for a running process.
14.6.1.2. Version 2: adding rodata#
Read-only data section .rodata
##### DATA SECTION : read and writable memory s
.section .data
.global rwdata
rwdata: .fill 4096,1,0xff # 4096 bytes each initialized with a value of 0xff
What would happen if we put all our data in the .rodata
section?… try it and find out
Here is the code
CODE: asm - exploringASlayout2.s
# To use this assemble and link it:
# as -g exploringASlayout1.s -o exploringASlayout1.o
# ld -g exploringASlayout1.o -o exploringASlayout1
# Then using two terminals:
# 1. use the nm command on the binary to see what addresses the linker
# placed all the symbols at Eg.
# $ nm exploringASlayout1
# 0000000000403000 D __bss_start
# 0000000000403000 D _edata
# 0000000000403000 D _end
# 0000000000402000 d rwdata
# 0000000000401000 T _start
# (if you like you can also run: "readelf -S <binary>" to
# see more information about the sections in the binary)
# 2. then run the binary -- its should stop on the read
# Eg.
# $ ./exploringASlayout1
#
# 3. in another terminal use ps and grep to find the process started from the binary
# Eg.
# $ ps auxgww | grep exploring
# jovyan 1697 0.0 0.0 168 4 pts/29 S+ 13:41 0:00 ./exploringASlayout1
# jovyan 1699 0.0 0.0 6440 720 pts/30 S+ 13:42 0:00 grep exploring
# $
# We see the process id (pid) is 1697
# 4. using the process id you can now examine the file "/proc/<pid>/maps" to
# see the layout of the running processes address space
# Eg.
# $ cat /proc/1697/maps
# 00400000-00401000 r--p 00000000 103:05 1189363 /home/jovyan/exploringASlayout1
# 00401000-00402000 r-xp 00001000 103:05 1189363 /home/jovyan/exploringASlayout1
# 00402000-00403000 rw-p 00002000 103:05 1189363 /home/jovyan/exploringASlayout1
# 7ffd13044000-7ffd13065000 rw-p 00000000 00:00 0 [stack]
# 7ffd13069000-7ffd1306d000 r--p 00000000 00:00 0 [vvar]
# 7ffd1306d000-7ffd1306f000 r-xp 00000000 00:00 0 [vdso]
# ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
# $
# What we see is that there are three mappings to the binary file, a mapping for the stack and then some
# extra's used by the OS.
# 5. you can now press enter in the terminal that the binary is running in
# this will cause the read to finish and the binary will then continue to execute
# CODE to explore how we control the address space mappings
# The smallest size of an address space mapping is called the page size
# on Linux the default is 4096 (4Kb). To make it easier to identify
# the mappings we will fill each section we add to a full page (4Kb)
# worth of bytes.
# This version 2 adds read only data section
# One page' of "text", one page of "data" and one pager of "readonly data"
# - text should get mapped to a region that is readable and executable (r-x)
# - data should get mapped to a region that is readable and writable (rw-)
# - rodata should get mapped to a region that is readable only (r--)
.intel_syntax noprefix # assembler syntax to use <directive>
.section .text # linker section <directive>
.global _start # linker symbol type <directive>
_start:
# use a read system call to stop the program until user presses enter
# so that we can examine things before and after the code runs
mov rax, 0 # read syscall = 0 -- read(fd, buf, len)
mov rdi, 0 # fd = rdi = 0 = stdin
mov rsi, OFFSET rwdata # buf = rsi = rwdata memory
mov rdx, 1 # len = rdx = 1 wait for one byte "enter"
syscall
mov rax, 60 # exit system call
mov rdi, 0 # exit status 0
syscall
# fill the text section up with zeros so that it is one page in size.
.org 4095 # force the next byte to be at the end of the page (4095
.byte 0x00 # put a byte of zero here
##### DATA SECTION : read and writable memory s
.section .data
.global rwdata
rwdata: .fill 4096,1,0xff # 4096 bytes each initialized with a value of 0xff
##### READ ONLY DATA SECTION : read only memmory
.section .rodata
.global rodata
rodata:
.fill 4096,1,'a # 4096 bytes each initialized with a value of ASCII lower case a
# the following tells the linux kernel to set permissions the way we expect
# rather than the defaults which map all sections as executable
# When using a compiler it will add this for us
.section .note.GNU-stack,"",@progbits
Repeat the steps from version 1 to see what has changed.
14.6.1.3. Version 3: adding bss#
The bss section is used to create a mapping for all the data that we need memory for but don’t need to specify an initial value that is not zero.
Read-Write memory .bss
##### BSS DATA SECTION: read and write automaitically added and filled with zero values
.section .bss
.global zerodata
zerodata: .fill 4096,1
# The above could be replaced with the single line: .comm zerodata, 4096, 1
Note the .comm
directive is a short cut that switch the section to .bss adds the symbol and makes it global
Here is the code
CODE: asm - exploringASlayout3.s
# To use this assemble and link it:
# as -g exploringASlayout1.s -o exploringASlayout1.o
# ld -g exploringASlayout1.o -o exploringASlayout1
# Then using two terminals:
# 1. use the nm command on the binary to see what addresses the linker
# placed all the symbols at Eg.
# $ nm exploringASlayout1
# 0000000000403000 D __bss_start
# 0000000000403000 D _edata
# 0000000000403000 D _end
# 0000000000402000 d rwdata
# 0000000000401000 T _start
# (if you like you can also run: "readelf -S <binary>" to
# see more information about the sections in the binary)
# 2. then run the binary -- its should stop on the read
# Eg.
# $ ./exploringASlayout1
#
# 3. in another terminal use ps and grep to find the process started from the binary
# Eg.
# $ ps auxgww | grep exploring
# jovyan 1697 0.0 0.0 168 4 pts/29 S+ 13:41 0:00 ./exploringASlayout1
# jovyan 1699 0.0 0.0 6440 720 pts/30 S+ 13:42 0:00 grep exploring
# $
# We see the process id (pid) is 1697
# 4. using the process id you can now examine the file "/proc/<pid>/maps" to
# see the layout of the running processes address space
# Eg.
# $ cat /proc/1697/maps
# 00400000-00401000 r--p 00000000 103:05 1189363 /home/jovyan/exploringASlayout1
# 00401000-00402000 r-xp 00001000 103:05 1189363 /home/jovyan/exploringASlayout1
# 00402000-00403000 rw-p 00002000 103:05 1189363 /home/jovyan/exploringASlayout1
# 7ffd13044000-7ffd13065000 rw-p 00000000 00:00 0 [stack]
# 7ffd13069000-7ffd1306d000 r--p 00000000 00:00 0 [vvar]
# 7ffd1306d000-7ffd1306f000 r-xp 00000000 00:00 0 [vdso]
# ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
# $
# What we see is that there are three mappings to the binary file, a mapping for the stack and then some
# extra's used by the OS.
# 5. you can now press enter in the terminal that the binary is running in
# this will cause the read to finish and the binary will then continue to execute
# CODE to explore how we control the address space mappings
# The smallest size of an address space mapping is called the page size
# on Linux the default is 4096 (4Kb). To make it easier to identify
# the mappings we will fill each section we add to a full page (4Kb)
# worth of bytes.
# This version 3 adds bss section
# One page' of "text", one page of "data" and one pager of "readonly data"
# - text should get mapped to a region that is readable and executable (r-x)
# - data should get mapped to a region that is readable and writable (rw-)
# - rodata should get mapped to a region that is readable only (r--)
# - bss should get mapped to a region that is readable and writable (rw-)
.intel_syntax noprefix # assembler syntax to use <directive>
.section .text # linker section <directive>
.global _start # linker symbol type <directive>
_start:
# use a read system call to stop the program until user presses enter
# so that we can examine things before and after the code runs
mov rax, 0 # read syscall = 0 -- read(fd, buf, len)
mov rdi, 0 # fd = rdi = 0 = stdin
mov rsi, OFFSET rwdata # buf = rsi = rwdata memory
mov rdx, 1 # len = rdx = 1 wait for one byte "enter"
syscall
mov rax, 60 # exit system call
mov rdi, 0 # exit status 0
syscall
# fill the text section up with zeros so that it is one page in size.
.org 4095 # force the next byte to be at the end of the page (4095
.byte 0x00 # put a byte of zero here
##### DATA SECTION : read and writable memory s
.section .data
.global rwdata
rwdata: .fill 4096,1,0xff # 4096 bytes each initialized with a value of 0xff
##### READ ONLY DATA SECTION : read only data
.section .rodata
.global rodata
rodata:
.fill 4096,1,'a # 4096 bytes each initialized with a value of ASCII lower case a
##### BSS DATA SECTION: read and write automaitically added and filled with zero values
.section .bss
.global zerodata
zerodata: .fill 4096,1
# The above could be replaced with the single line: .comm zerodata, 4096, 1
# the following tells the linux kernel to set permissions the way we expect
# rather than the defaults which map all sections as executable
# When using a compiler it will add this for us
.section .note.GNU-stack,"",@progbits
Repeat the steps from version 1 and see what what has changed compared to versions 1 and 2
14.6.1.4. Version 4: And and Grow our Heap#
Now let’s explore how while our program is running we can get more memory added to our process.
The brk
system call - move “break pointer”
initially the data segment of your process will end at some location based on what data sections you defined
this location is called the program break pointer
the location where valid data memory ends and the un-allocated virtual address space
with
brk
you can set the end to a value bigger than it starting locationyou can also shrink the break pointer back to remove memory
This area is called the HEAP
call
brk
with argument of 0 and Linux kernel returns current break addresscall
brk
“with a reasonable address” and the kernel will allocated or de-allocated memory for us
in reality once we move to C we never directly call “brk” rather we will use a library routines called :
malloc
andfree
which will callbrk
for us.
As usual you can find more info in the manual
You can find the system call number and what the arguments are here:
Lets add this code
What does it do?
# Now add a Heap
# get current break pointer
xor rdi, rdi # pass 0 to brk (invalid request)
mov rax, 12 # brk syscall number 12
syscall # call brk with 0
mov r15, rax # keep a copy of where our new memory starts
# so we can use it later
# now add some memory by requesting to increase the break
# address by 32 bytes
mov rdi, rax # mov current break into rdi
add rdi, 4096 # ask for 4096 bytes rdi=rdi+4096
mov rax, 12 # brk sycall 12
syscall
CODE: asm - exploringASlayout4.s
# To use this assemble and link it:
# as -g exploringASlayout1.s -o exploringASlayout1.o
# ld -g exploringASlayout1.o -o exploringASlayout1
# Then using two terminals:
# 1. use the nm command on the binary to see what addresses the linker
# placed all the symbols at Eg.
# $ nm exploringASlayout1
# 0000000000403000 D __bss_start
# 0000000000403000 D _edata
# 0000000000403000 D _end
# 0000000000402000 d rwdata
# 0000000000401000 T _start
# (if you like you can also run: "readelf -S <binary>" to
# see more information about the sections in the binary)
# 2. then run the binary -- its should stop on the read
# Eg.
# $ ./exploringASlayout1
#
# 3. in another terminal use ps and grep to find the process started from the binary
# Eg.
# $ ps auxgww | grep exploring
# jovyan 1697 0.0 0.0 168 4 pts/29 S+ 13:41 0:00 ./exploringASlayout1
# jovyan 1699 0.0 0.0 6440 720 pts/30 S+ 13:42 0:00 grep exploring
# $
# We see the process id (pid) is 1697
# 4. using the process id you can now examine the file "/proc/<pid>/maps" to
# see the layout of the running processes address space
# Eg.
# $ cat /proc/1697/maps
# 00400000-00401000 r--p 00000000 103:05 1189363 /home/jovyan/exploringASlayout1
# 00401000-00402000 r-xp 00001000 103:05 1189363 /home/jovyan/exploringASlayout1
# 00402000-00403000 rw-p 00002000 103:05 1189363 /home/jovyan/exploringASlayout1
# 7ffd13044000-7ffd13065000 rw-p 00000000 00:00 0 [stack]
# 7ffd13069000-7ffd1306d000 r--p 00000000 00:00 0 [vvar]
# 7ffd1306d000-7ffd1306f000 r-xp 00000000 00:00 0 [vdso]
# ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
# $
# What we see is that there are three mappings to the binary file, a mapping for the stack and then some
# extra's used by the OS.
# 5. you can now press enter in the terminal that the binary is running in
# this will cause the read to finish and the binary will then continue to execute
# CODE to explore how we control the address space mappings
# The smallest size of an address space mapping is called the page size
# on Linux the default is 4096 (4Kb). To make it easier to identify
# the mappings we will fill each section we add to a full page (4Kb)
# worth of bytes.
# This version 4 dyamically adds a "heap" area of memory
# One page' of "text", one page of "data" and one pager of "readonly data"
# - text should get mapped to a region that is readable and executable (r-x)
# - data should get mapped to a region that is readable and writable (rw-)
# - rodata should get mapped to a region that is readable only (r--)
# - bss should get mapped to a region that is readable and writable (rw-)
# - heap the brk system call is used to add a heap region that is
.intel_syntax noprefix # assembler syntax to use <directive>
.section .text # linker section <directive>
.global _start # linker symbol type <directive>
_start:
# use a read system call to stop the program until user presses enter
# so that we can examine things before and after the code runs
mov rax, 0 # read syscall = 0 -- read(fd, buf, len)
mov rdi, 0 # fd = rdi = 0 = stdin
mov rsi, OFFSET rwdata # buf = rsi = rwdata memory
mov rdx, 1 # len = rdx = 1 wait for one byte "enter"
syscall
# Now add a Heap
# get current break pointer
xor rdi, rdi # pass 0 to brk (invalid request)
mov rax, 12 # brk syscall number 12
syscall # call brk with 0
mov r15, rax # keep a copy of where our new memory starts
# so we can use it later
# now add some memory by requesting to increase the break
# address by 32 bytes
mov rdi, rax # mov current break into rdi
add rdi, 4096 # ask for 4096 bytes rdi=rdi+4096
mov rax, 12 # brk sycall 12
syscall
# use a read system call to stop the program until user presses enter
# so that we can examine things before and after the code runs
mov rax, 0 # read syscall = 0 -- read(fd, buf, len)
mov rdi, 0 # fd = rdi = 0 = stdin
mov rsi, OFFSET rwdata # buf = rsi = rwdata memory
mov rdx, 1 # len = rdx = 1 wait for one byte "enter"
syscall
mov rax, 60 # exit system call
mov rdi, 0 # exit status 0
syscall
# fill the text section up with zeros so that it is one page in size.
.org 4095 # force the next byte to be at the end of the page (4095
.byte 0x00 # put a byte of zero here
##### DATA SECTION : read and writable memory s
.section .data
.global rwdata
rwdata: .fill 4096,1,0xff # 4096 bytes each initialized with a value of 0xff
##### READ ONLY DATA SECTION : read only data
.section .rodata
.global rodata
rodata:
.fill 4096,1,'a # 4096 bytes each initialized with a value of ASCII lower case a
##### BSS DATA SECTION: read and write automaitically added and filled with zero values
.section .bss
.global zerodata
zerodata: .fill 4096,1
# The above could be replaced with the single line: .comm zerodata, 4096, 1
# the following tells the linux kernel to set permissions the way we expect
# rather than the defaults which map all sections as executable
# When using a compiler it will add this for us
.section .note.GNU-stack,"",@progbits
We have added more code so that you can pause the program before and after it makes the calls to brk so that you can see that heap get added.
start the program running
follow the same steps as the other versions to look at the address space layout
then press enter – this will let the program continue. It will then run the code we added to add the heap area and pause again
again examine the address space layout – do you see the difference?
press enter again – this will again let the program continue and it will exit
14.6.1.5. Version 5: giving memory back – shrink the heap back down#
In this final version we add a final step to the program to shrink the heap back to its original size (0). This removes it from our process and give the memory back to the OS.
Here is the code we add – remember when we first called brk
we stored the original location in r15
# remove memory -- shrink heap back down to its original
mov rdi, r15 # set break pointer back to the original location
mov rax, 12 # brk syscall number 12
syscall
CODE: asm - exploringASlayout5.s
# To use this assemble and link it:
# as -g exploringASlayout1.s -o exploringASlayout1.o
# ld -g exploringASlayout1.o -o exploringASlayout1
# Then using two terminals:
# 1. use the nm command on the binary to see what addresses the linker
# placed all the symbols at Eg.
# $ nm exploringASlayout1
# 0000000000403000 D __bss_start
# 0000000000403000 D _edata
# 0000000000403000 D _end
# 0000000000402000 d rwdata
# 0000000000401000 T _start
# (if you like you can also run: "readelf -S <binary>" to
# see more information about the sections in the binary)
# 2. then run the binary -- its should stop on the read
# Eg.
# $ ./exploringASlayout1
#
# 3. in another terminal use ps and grep to find the process started from the binary
# Eg.
# $ ps auxgww | grep exploring
# jovyan 1697 0.0 0.0 168 4 pts/29 S+ 13:41 0:00 ./exploringASlayout1
# jovyan 1699 0.0 0.0 6440 720 pts/30 S+ 13:42 0:00 grep exploring
# $
# We see the process id (pid) is 1697
# 4. using the process id you can now examine the file "/proc/<pid>/maps" to
# see the layout of the running processes address space
# Eg.
# $ cat /proc/1697/maps
# 00400000-00401000 r--p 00000000 103:05 1189363 /home/jovyan/exploringASlayout1
# 00401000-00402000 r-xp 00001000 103:05 1189363 /home/jovyan/exploringASlayout1
# 00402000-00403000 rw-p 00002000 103:05 1189363 /home/jovyan/exploringASlayout1
# 7ffd13044000-7ffd13065000 rw-p 00000000 00:00 0 [stack]
# 7ffd13069000-7ffd1306d000 r--p 00000000 00:00 0 [vvar]
# 7ffd1306d000-7ffd1306f000 r-xp 00000000 00:00 0 [vdso]
# ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
# $
# What we see is that there are three mappings to the binary file, a mapping for the stack and then some
# extra's used by the OS.
# 5. you can now press enter in the terminal that the binary is running in
# this will cause the read to finish and the binary will then continue to execute
# CODE to explore how we control the address space mappings
# The smallest size of an address space mapping is called the page size
# on Linux the default is 4096 (4Kb). To make it easier to identify
# the mappings we will fill each section we add to a full page (4Kb)
# worth of bytes.
# This version 4 dyamically adds a "heap" area of memory
# One page' of "text", one page of "data" and one pager of "readonly data"
# - text should get mapped to a region that is readable and executable (r-x)
# - data should get mapped to a region that is readable and writable (rw-)
# - rodata should get mapped to a region that is readable only (r--)
# - bss should get mapped to a region that is readable and writable (rw-)
# - heap the brk system call is used to add a heap region that is
.intel_syntax noprefix # assembler syntax to use <directive>
.section .text # linker section <directive>
.global _start # linker symbol type <directive>
_start:
# use a read system call to stop the program until user presses enter
# so that we can examine things before and after the code runs
mov rax, 0 # read syscall = 0 -- read(fd, buf, len)
mov rdi, 0 # fd = rdi = 0 = stdin
mov rsi, OFFSET rwdata # buf = rsi = rwdata memory
mov rdx, 1 # len = rdx = 1 wait for one byte "enter"
syscall
# Now add a Heap
# get current break pointer
xor rdi, rdi # pass 0 to brk (invalid request)
mov rax, 12 # brk syscall number 12
syscall # call brk with 0
mov r15, rax # keep a copy of where our new memory starts
# so we can use it later
# now add some memory by requesting to increase the break
# address by 32 bytes
mov rdi, rax # mov current break into rdi
add rdi, 4096 # ask for 4096 bytes rdi=rdi+4096
mov rax, 12 # brk sycall 12
syscall
# use a read system call to stop the program until user presses enter
# so that we can examine things before and after the code runs
mov rax, 0 # read syscall = 0 -- read(fd, buf, len)
mov rdi, 0 # fd = rdi = 0 = stdin
mov rsi, OFFSET rwdata # buf = rsi = rwdata memory
mov rdx, 1 # len = rdx = 1 wait for one byte "enter"
syscall
# remove memory -- shrink heap back down to its original
mov rdi, r15 # set break pointer back to the original location
mov rax, 12 # brk syscall number 12
syscall
# one last read so that we can see what the as looks like prior to exiting
# use a read system call to stop the program until user presses enter
# so that we can examine things before and after the code runs
mov rax, 0 # read syscall = 0 -- read(fd, buf, len)
mov rdi, 0 # fd = rdi = 0 = stdin
mov rsi, OFFSET rwdata # buf = rsi = rwdata memory
mov rdx, 1 # len = rdx = 1 wait for one byte "enter"
syscall
mov rax, 60 # exit system call
mov rdi, 0 # exit status 0
syscall
# fill the text section up with zeros so that it is one page in size.
.org 4095 # force the next byte to be at the end of the page (4095
.byte 0x00 # put a byte of zero here
##### DATA SECTION : read and writable memory s
.section .data
.global rwdata
rwdata: .fill 4096,1,0xff # 4096 bytes each initialized with a value of 0xff
##### READ ONLY DATA SECTION : read only data
.section .rodata
.global rodata
rodata:
.fill 4096,1,'a # 4096 bytes each initialized with a value of ASCII lower case a
##### BSS DATA SECTION: read and write automaitically added and filled with zero values
.section .bss
.global zerodata
zerodata: .fill 4096,1
# The above could be replaced with the single line: .comm zerodata, 4096, 1
# the following tells the linux kernel to set permissions the way we expect
# rather than the defaults which map all sections as executable
# When using a compiler it will add this for us
.section .note.GNU-stack,"",@progbits
Again we added another read so that you can explore what the address space looks like after we shrink the heap but before we exit.
So that’s it – we have seen how memory gets added to the process based on what sections we added in our source code and how we can make calls to the OS to add and remove “heap” memory while our program is running.
There is a more advanced call in UNIX called mmap
but we will leave it’s discussion for another time.