3-types-alignment-pointers-web
3-types-alignment-pointers-web
CS 61: Lecture 3
9/13/2023
CPU
RAM Value
Recap: Memory
• A program’s address space has four Address
segments space
• The code segment stores the program’s CPU Byte N-1
instructions Stack
• The static data segment contains global
variables
• The stack contains bookkeeping information
(e.g., local variables) for active functions: More
details in the next lecture!
• The heap contains dynamically allocated data
Pulled
• The lifetime of a C++ object depends on from Heap
which segment it lives in! executable
into RAM
• Code and static data is born at program start during (6) Static data
and only dies when the program dies
• The compiler automatically ensures that a live
function’s stack data is created when the Code
function starts, and only lives as long as the Byte 0
function SSD
• A programmer has to manually allocate and
Heap Allocation and Deallocation
(Slightly simplified)
• The OS keeps track of the sizes and
locations of the four segments (code, static
data, heap, and stack)
• The C++ runtime tracks the locations of
free space in the heap
• During a call to new <type> . . .
• A language runtime contains code that is:
• If the heap has sizeof(<type>) contiguous free
• Provided by the language implementation itself
bytes, the C++ runtime simply allocates those
bytes • Handles bookkeeping tasks on behalf of developer-written
code
• Otherwise, the C++ runtime must first invoke a
system call like sbrk()• to
Examples
ask the OSoftoruntime
increasecode are:
the size of the heap • C++ new/delete operators that directly handle low-level
heap management
• During a call to delete <var_name> ...
• C++ code which initializes global variables to all-zero bytes
• The C++ runtime deallocates the relevant heap
• C++ IO functionality (e.g., printf(), cout) that provides
bytes simplified interfaces to IO-related system calls
• The runtime may also use• aC++ system
datacall to
structures like vector
shrink the heap, although many runtimes don’t
Heap Allocation and Deallocation
(Slightly simplified) //new operator
//calls sbrk(8)
• The OS keeps track of the sizes and //to increase
locations of the four segments (code, static //heap size by
data, heap, and stack) //8 bytes
• The C++ runtime tracks the locations of
free space in the heap //Suppose that 0x03
• During a call to new <type> . . . //heap has no
//free bytes 0x04
• If the heap has sizeof(<type>) contiguous free p2
bytes, the C++ runtime simply allocates those //and then we 0x1A
bytes //do . . .
• Otherwise, the C++ runtime must first invoke a int* p1 = new int;
0xCA
system call like sbrk() to ask the OS to increase int* p2 = new int;
0x17
the size of the heap
*p1 = 389324381; 0x34
• During a call to delete <var_name> . . . *p2 = 50600650;
p1
• The C++ runtime deallocates the relevant heap 0x9E
bytes delete p1; 0x5D brk
• The runtime may also use a system call to delete p2;
shrink the heap if there are free bytes at the pointer
What happens if the
program then does this . . .
printf(“%d\n”, *p1);
. . .?
According
UNDEFINED to the C++ standard,
BEHAVIOR
“When == the end of the duration of
MONSTER
a region of storage is
NOBODY
reached,
PARTY
KNOWS WHAT
the values of all pointers
THESEof
representing the address FOUL
any
part of that region THINGS WANT
of storage
become invalid pointer values.
Indirection through
Undefined DOan invalid
NOT BRING
behavior
pointer value and passing an
THEM INTO
invalid pointer value to a
YOUR
deallocation function have
Undefine
d
LIFE
behavior
When your program performs an undefined action, your
program is not guaranteed to deterministically perform
the same behavior (e.g., hang, crash, emit smoke)
during each program run.
SOMETHING
UNDEFINED.
Q: What happens if you forget to
deallocate
memory
A: You get athat you no
memory longer need?
leak!
THIS WILL HAPPEN TO YOU IN YOUR
LIFE.
C++: Primitive Types vs. Compound Types
• A type describes two things #include <cstdio>
• A set of possible values #include "hexdump.hh"
int main() {
• A set of valid operations on those values
int arr[2] = {61, 62};
• A primitive type is not composed of hexdump_object(arr);
other types (e.g., char, float)
struct {
• A compound type can aggregate int a;
one or more primitive types int b;
• An array represents a collection of char c;
values: char d;
• Having the same type } s = {61, 62, 63, 64};
• Accessed via indexing hexdump_object(s);
• A struct represents a collection of hexdump_object(s.a);
values:
• Having potentially different types return 0;
• Accessed via field names }
Console 7ffc72c1cc08 3d 00 00 00 3e 00 00 00 |=...>...|
^^^^^^^^^^^^ 3d 00 00 00 3e ^^^^^^^^
7ffc72c1cbfc ^^^^^^
00 00 00 3f 40 00 00 |=...>...?@..|
output: Address of starting
7ffc72c1cbfc 3d 00 value
00 00 Hex representation ASCII representation
|=...|
C++: Object Layout
• The C++ standard restricts how #include <cstdio>
#include "hexdump.hh"
the compiler and runtime can int main() {
position objects in memory int arr[2] = {61, 62};
• Two objects cannot overlap in hexdump_object(arr);
memory: a particular byte in
memory belongs to no object, or struct {
exactly one object int a;
int b;
…
…
char c;
int p int p char d;
} s = {61, 62, 63, 64};
hexdump_object(s);
int q int q
hexdump_object(s.a);
… return 0;
…
}
Console 7ffc72c1cc08 3d 00 00 00 3e 00 00 00 |=...>...|
7ffc72c1cbfc 3d 00 00 00 3e 00 00 00 3f 40 00 00 |=...>...?@..|
output: 7ffc72c1cbfc 3d 00 00 00 |=...|
C++: Object Layout
• The C++ standard restricts how the #include <cstdio>
compiler and runtime can position #include "hexdump.hh"
int main() {
objects in memory int arr[2] = {61, 62};
• Two objects cannot overlap in hexdump_object(arr);
memory: a particular byte in memory
belongs to no object, or exactly one struct {
object int a;
• For an array holding elements of type int b;
T: char c;
Addrs
• Each value is arranged sequentially and char d;
0x107
0x106 back-to-back } s = {61, 62, 63, 64};
0x105 • Given an array with starting address a, the hexdump_object(s);
0x104
i-th element (i.e., the element a[i]) lives hexdump_object(s.a);
0x103
0x102
at memory location a + (i * sizeof(T))
0x101 • The total amount of memory consumed by return 0;
0x100 the array
char* a = newischar[8];
N * sizeof(T)
//new returns 0x100 }
Console 7ffc72c1cc08 3d 00 00 00 3e 00 00 00 |=...>...|
7ffc72c1cbfc 3d 00 00 00 3e 00 00 00 3f 40 00 00 |=...>...?@..|
output: 7ffc72c1cbfc 3d 00 00 00 |=...|
C++: Object Layout
• The C++ standard restricts how the #include <cstdio>
compiler and runtime can position #include "hexdump.hh"
int main() {
objects in memory int arr[2] = {61, 62};
• Two objects cannot overlap in memory: hexdump_object(arr);
a particular byte in memory belongs to
no object, or exactly one object struct {
• For an array holding elements of type int a;
T: int b;
• Each value is arranged sequentially and char c;
back-to-back char d;
• Given an array with starting address a, the } s = {61, 62, 63, 64};
i-th element (i.e., the element a[i]) lives hexdump_object(s);
at memory location a + (i * sizeof(T)) hexdump_object(s.a);
• The total amount of memory consumed by
the array is N * sizeof(T)
return 0;
• struct fields are laid out sequentially }
but maybe not adjacently—more on
Console
this later!
7ffc72c1cc08 3d 00 00 00 3e 00 00 00 |=...>...|
7ffc72c1cbfc 3d 00 00 00 3e 00 00 00 3f 40 00 00 |=...>...?@..|
output: 7ffc72c1cbfc 3d 00 00 00 |=...|
#include <cstdio>
C++ Unions #include "hexdump.hh"
int main() {
• Like a struct, a union struct {
union {
aggregates primitive types char c;
double d;
• Unlike a struct, a union only } u;
has enough space to store the bool is_char; //The tag!
} s;
largest of its contained types! s.u.c = ‘J’;
• At any given moment, only one s.is_char = true;
of the possible values in a union
is valid printf(“%zu\t”, sizeof(s.u));
printf("%zu\n", sizeof(s.is_char));
• In practice, unions are often printf(“%zu\n”, sizeof(s));
associated with a tag to indicate hexdump_object(s);
which union field is valid return 0;
}
8 1
Console 16
output:
• Hmm . . . if sizeof(s.u) is 8 and #include <cstdio>
sizeof(s.is_char) is 1, then why #include "hexdump.hh"
int main() {
is sizeof(s) not 8+1=9? struct {
• The answer is alignment! union {
char c;
• On modern computers, a CPU fetches double d;
data from RAM in units of cache lines } u;
(e.g., 64 bytes on a modern x86-64 bool is_char; //The tag!
processor) } s;
• If a primitive value spanned two s.u.c = ‘J’;
adjacent cache lines, then reading or s.is_char = true;
writing that value would require two
RAM accesses (which would be slower printf(“%zu\t”, sizeof(s.u));
than just one) printf("%zu\n", sizeof(s.is_char));
printf(“%zu\n”, sizeof(s));
• So, the C++ standard imposes
hexdump_object(s);
alignment restrictions on the locations return 0;
at which primitives and compound }
values may start
8 1
Console 16
output:
• Hmm . . . if sizeof(s.u) is 8 and #include <cstdio>
sizeof(s.is_char) is 1, then #include "hexdump.hh"
int main() {
why is sizeof(s) not 8+1=9? struct {
• The answer is alignment! union {
char c;
• On modern computers, a CPU fetches double d;
data from RAM in units of cache lines } u;
(e.g., 64 bytes on a modern x86-64 bool is_char; //The tag!
processor) } s;
• If a primitive value spanned two s.u.c = ‘J’;
adjacent cache lines, then reading or s.is_char = true;
writing that value would require two
printf(“%zu\t”, sizeof(s.u));
RAM accesses (which would be slower printf("%zu\n", sizeof(s.is_char));
than just one) printf(“%zu\n”, sizeof(s));
• So, the C++ standard imposes hexdump_object(s);
alignment restrictions on the return 0;
locations at which primitives and }
compound values
8 may
1 start
Console 16
output: 7ffea59a86e0 4a 87 9a a5 fe 7f 00 00 01 00 00 00 00 00 00 00 \
|J...............|
C++: Memory Alignment
• A type T has an alignment requirement of A if any
value of type T must start at a memory address that is
evenly divisible by A
• The C++ standard says that all alignments must be powers
of two
• So, a particular valid alignment is aligned with all smaller
alignments
• Ex: A type with an alignment of 16 bytes also satisfies an
alignment of 8 bytes
• Each primitive type T has an alignment of alignof(T)
C++: Memory Alignment
• A type T has an alignment requirement of A if any value of type T
must start at a memory address that is evenly divisible by A
• The C++ standard says that all alignments must be powers of two
• So, a particular valid alignment is aligned with all smaller alignments
• Ex: A type with an alignment of 16 bytes also satisfies an alignment of 8 bytes
• Each primitive type T has an alignment of alignof(T)
• Each element of a compound object must satisfy alignment, so:
• Array: alignof(T[N]) == alignof(T)
• Struct: alignof(struct{T0,T1,…,TN}) == maxi(alignof(Ti))
• This rule ensures that the first member of the struct is aligned
• However, aligning subsequent members may require padding between members
• A funny side effect is that you can sometimes change a struct’s size by reordering
its members!
Padding Example
struct { struct {
char a; //alignof(char): 1 char a; //alignof(char): 1
int b; //alignof(int): 4 char _pad0[3]; //padding to
short c;//alignof(short):2 //make s.b start on
//4-byte boundary
char d; //alignof(char): 1
int b; //alignof(int): 4
} s; short c;//alignof(short):2
• Any starting location for s will satisfy the char d; //alignof(char): 1
alignment properties for s.a and s.d char _pad1[1]; //padding to
• However, arbitrary starting locations for //make sizeof(s) a
s are not guaranteed to ensure //multiple of the
alignment for s.b and s.c! //alignment (which
• So, the compiler pretends that s starts at //is 4, because that
address 0, and then . . . //is the largest
• adds padding between s’s members, and //alignment of any
• possibly adds padding at the end
//struct member
. . . such that: };
• aligned locations for s also result in //sizeof(s): 12
alignment for all of s’s members, and
• sizeof(s) is a multiple of the alignof(s) //alignof(s): 4
Alignment is the reason that #include <cstdio>
sizeof(s) is 16! #include "hexdump.hh"
• The largest alignment of any int main() {
struct {
element in s is union {
alignof(double) == 8 char c;
• So, s’s alignment is 8 double d;
} u;
• The “natural” (i.e., unpadded) bool is_char; //The tag!
size of s is 8+1 = 9, but 9 is } s;
not evenly divisible by s’s s.u.c = ‘J’;
alignment 8, so we must pad s.is_char = true;
the object to be 16 bytes
printf(“%zu\t”, sizeof(s.u));
printf("%zu\n", sizeof(s.is_char));
printf(“%zu\n”, sizeof(s));
hexdump_object(s);
return 0;
}
8 1 .u.c (the ‘J’ char) .is_char Padding!
Console 16
output: 7ffea59a86e0 4a 87 9a a5 fe 7f 00 00 01 00 00 00 00 00 00 00 \
|J...............|
The C++ standard requires that the
expression new T will return an address that
satisfies alignof(T)!
malloc(size) must return memory that is aligned
for any object! In practice, this means that the
memory must satisfy alignof(std::max_align_t)
== 16 on x86-64!