Pointers and arrays are a confusing subject in C. Pointers can be of different types, which makes possible pointer arithmetic to access necessary elements in an array in different ways. Consider the following examples:
int *A[5]; This is an array of 5 pointers to integers.
int (*B)[5]; This is a pointer to an array of 5 integers.
We are interested in the latter one, so let's analyze the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <stdio.h> int main(int argc, char* argv[]){ /* Array of integers */ int arr[] = {10,11,12,13,14}; /* Number of elements in the array */ int N = sizeof(arr)/sizeof(arr[0]); /* Pointer to an array of integers */ int (*pa)[N] = &arr; /* Pointer to the first element of the array */ /* Equivalent to "int *pi = &arr[0]" */ int *pi = arr; for(int i = 0; i < N; i++) { printf("Element %d of the array: arr[%d]=%d, *(pi+%d)=%d, (*pa)[%d]=%d\n", i, i, arr[i], i, *(pi+i), i, (*pa)[i]); } return 0; } |
It will print all elements of the array using different pointer dereferencing strategies.
1 2 3 4 5 6 7 |
[johndoe@ArchLinux]% gcc Pointer_to_Array.c -o Pointer_to_Array -g [johndoe@ArchLinux]% ./Pointer_to_Array Element 0 of the array: arr[0]=10, *(pi+0)=10, (*pa)[0]=10 Element 1 of the array: arr[1]=11, *(pi+1)=11, (*pa)[1]=11 Element 2 of the array: arr[2]=12, *(pi+2)=12, (*pa)[2]=12 Element 3 of the array: arr[3]=13, *(pi+3)=13, (*pa)[3]=13 Element 4 of the array: arr[4]=14, *(pi+4)=14, (*pa)[4]=14 |
If the variables stored in the memory are analyzed from the assembly perspective, it becomes very clear what pointers actually do.
To begin with, we should consider the notion of a variable and lvalues. In the C code above, variables "arr", "pi", "pa" stand for a synonym of an address. So after initializing the array "arr" on the stack, the memory will look like this:
1 2 3 4 5 |
0x00007fffffffe2d0: 0x0000000a 0x00007fffffffe2d4: 0x0000000b 0x00007fffffffe2d8: 0x0000000c 0x00007fffffffe2dc: 0x0000000d 0x00007fffffffe2e0: 0x0000000e |
So "arr" is just a synonym for 0x7fffffffe2d0. Similarly, initialized pointers will look like this:
1 2 |
0x00007fffffffe2f8: 0x00007fffffffe2d0 0x00007fffffffe2f0: 0x00007fffffffe2d0 |
and should be read as:
1 2 |
pa: 0x00007fffffffe2d0 pi: 0x00007fffffffe2d0 |
Because "pa" is just a name for 0x7fffffffe2f8 and "pi" is a name for 0x7fffffffe2f0. Notice that both "pa" and "pi" store the same value: the address (0x7fffffffe2d0) of the first element (10, hexadecimal "A") of the array. So both pointers point at the same place - at the beginning of the array. However, they are of different type (int * and int (*)[5]), so Incrementing "pi" by 1 yields 0x7fffffffe2d4, incrementing "pa" by 1 yields 0x7fffffffe2e4. In the first case, 4 bytes (the size of one integer) were added, in the second: 20 bytes (the size of 5 integers) .
Anyways, coming back to the original code. Before returning, the main function calls "printf" with 8 arguments. It's a good example to see how the argument passing conventions are applied. According to "System V Application Binary Interface: AMD64 Architecture Processor Supplement" (Linux AMD64 ABI), the arguments to a function a passed via registers and the stack. Our printf will be called using the following arguments:
Arg. # | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
Arg. | string | i | i | arr[i] | i | *(pi+i) | i | (*pa)[i] |
Register | RDI | RSI | RDX | RCX | R8 | R9 | Stack | Stack |
Note that last two arguments are pushed to the stack in the opposite order, so when the called function takes control over execution, it pops out the first argument, because it was on the top of the stack, then the second and so on. It was done to handle functions with variable number of arguments (like printf).
Eventually, I analyzed code with radare2 and renamed all local variables on the stack. On the right side there are debugging comments from GDB, interlaced with my comments on pointers. RBX register and variable "N_Minus_One" are not used, so consider them as junk.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
[0x004004d7]> pdf ;-- main: / (fcn) main 216 | main (); | ; var int argv @ rbp-0x60 | ; var int argc @ rbp-0x54 | ; var int arr[0] @ rbp-0x50 | ; var int arr[1] @ rbp-0x4c | ; var int arr[2] @ rbp-0x48 | ; var int arr[3] @ rbp-0x44 | ; var int arr[4] @ rbp-0x40 | ; var int pi @ rbp-0x30 | ; var int pa @ rbp-0x28 | ; var int N_Minus_One @ rbp-0x20 | ; var int N @ rbp-0x18 | ; var int i @ rbp-0x14 | ; var int CalleeSaved @ rbp-0x8 | ; DATA XREF from 0x0040041d (entry0) | 0x004004d7 55 push rbp ; Pointer_to_Array.c:3 int main(int argc, char* argv[]){ | 0x004004d8 4889e5 mov rbp, rsp | 0x004004db 53 push rbx ; Same as "mov qword [CalleeSaved], rbx" | 0x004004dc 4883ec58 sub rsp, 0x58 ; Allocate memory for local variables | 0x004004e0 897dac mov dword [argc], edi | 0x004004e3 488975a0 mov qword [argv], rsi | 0x004004e7 c745b00a0000. mov dword [arr[0]], 0xa ; Pointer_to_Array.c:5 int arr[] = {10,11,12,13,14}; | 0x004004ee c745b40b0000. mov dword [arr[1]], 0xb | 0x004004f5 c745b80c0000. mov dword [arr[2]], 0xc | 0x004004fc c745bc0d0000. mov dword [arr[3]], 0xd | 0x00400503 c745c00e0000. mov dword [arr[4]], 0xe | 0x0040050a c745e8050000. mov dword [N], 5 ; Pointer_to_Array.c:8 int N = sizeof(arr)/sizeof(arr[0]); | 0x00400511 8b45e8 mov eax, dword [N] ; Pointer_to_Array.c:11 int (*pa)[N] = &arr; | 0x00400514 4863d0 movsxd rdx, eax ; Not used | 0x00400517 4883ea01 sub rdx, 1 ; Not used | 0x0040051b 488955e0 mov qword [N_Minus_One], rdx ; Not used | 0x0040051f 4898 cdqe | 0x00400521 4889c1 mov rcx, rax | 0x00400524 bb00000000 mov ebx, 0 | 0x00400529 488d45b0 lea rax, qword [arr[0]] | 0x0040052d 488945d8 mov qword [pa], rax ; pa <= (void *)&arr[0], a generic pointer | 0x00400531 488d45b0 lea rax, qword [arr[0]] ; Pointer_to_Array.c:15 int *pi = arr; | 0x00400535 488945d0 mov qword [pi], rax ; pi <= (void *)&arr[0], a generic pointer | 0x00400539 c745ec000000. mov dword [i], 0 ; Pointer_to_Array.c:17 for(int i = 0; i < N; i++) { | ,=< 0x00400540 eb5a jmp 0x40059c | | ; JMP XREF from 0x004005a2 (main) | .--> 0x00400542 488b45d8 mov rax, qword [pa] ; Pointer_to_Array.c:18 printf("Element %d of the array: arr[%d]=%d, *(pi+%d)=%d, (*pa)[%d]=%d\n", i, i, arr[i], i, *(pi+i), i, (*pa)[i]); | || 0x00400546 8b55ec mov edx, dword [i] | || 0x00400549 4863d2 movsxd rdx, edx ; rdx <= i | || 0x0040054c 8b3490 mov esi, dword [rax + rdx*4]; esi <= *((int *)((void *)pa + 4*i)), type int is 32-bit long | || 0x0040054f 8b45ec mov eax, dword [i] ; eax <= i | || 0x00400552 4898 cdqe ; rax <= eax, Convert Doubleword to Quadword (just a sign extension) | || 0x00400554 488d14850000. lea rdx, qword [rax*4] ; rdx <= 4*i ("lea" instruction used for multiplication) | || 0x0040055c 488b45d0 mov rax, qword [pi] ; rax <= pi | || 0x00400560 4801d0 add rax, rdx ; rax <= pi + i, equivalent to (void *)pi + 4*i | || 0x00400563 448b00 mov r8d, dword [rax] ; r8d <= *(pi + 1), equivalent to *((int *)((void *)pi + 4*i)) | || 0x00400566 8b45ec mov eax, dword [i] | || 0x00400569 4898 cdqe | || 0x0040056b 8b4c85b0 mov ecx, dword [arr[0] + rax*4] ; ecx <= arr[i], equivalent to *((int *)((void *)&arr[0] + 4*i)) | || 0x0040056f 8b7dec mov edi, dword [i] | || 0x00400572 8b55ec mov edx, dword [i] | || 0x00400575 8b45ec mov eax, dword [i] | || 0x00400578 56 push rsi ; push *((int *)((void *)pa + 4*i)) from the value of esi | || 0x00400579 8b75ec mov esi, dword [i] | || 0x0040057c 56 push rsi ; push i | || 0x0040057d 4589c1 mov r9d, r8d | || 0x00400580 4189f8 mov r8d, edi | || 0x00400583 89c6 mov esi, eax | || 0x00400585 bf38064000 mov edi, str.Element__d_of_the_array:_arr__d___d____pi__d___d____pa___d___d_n ; "Element %d of the array: arr[%d]=%d, *(pi+%d)=%d, (*pa)[%d]=%d." @ 0x400638 | || 0x0040058a b800000000 mov eax, 0 | || 0x0040058f e85cfeffff call sym.imp.printf ; int printf(const char *format) | || 0x00400594 4883c410 add rsp, 0x10 | || 0x00400598 8345ec01 add dword [i], 1 ; Pointer_to_Array.c:17 for(int i = 0; i < N; i++) { | !| ; JMP XREF from 0x00400540 (main) | |<code>-> 0x0040059c 8b45ec mov eax, dword [i] | | 0x0040059f 3b45e8 cmp eax, dword [N] | </code>==< 0x004005a2 7c9e jl 0x400542 | 0x004005a4 b800000000 mov eax, 0 ; Pointer_to_Array.c:20 return 0; | 0x004005a9 488b5df8 mov rbx, qword [CalleeSaved] ; Pointer_to_Array.c:21 } | 0x004005ad c9 leave \ 0x004005ae c3 ret [0x004004d7]> |
How about removing initialized but unused variables? Let's compile with an optimization flag:
1 2 3 4 5 6 7 |
[johndoe@ArchLinux]% gcc Pointer_to_Array.c -o Pointer_to_Array -g -O1 [johndoe@ArchLinux]% ./Pointer_to_Array Element 0 of the array: arr[0]=10, *(pi+0)=10, (*pa)[0]=10 Element 1 of the array: arr[1]=11, *(pi+1)=11, (*pa)[1]=11 Element 2 of the array: arr[2]=12, *(pi+2)=12, (*pa)[2]=12 Element 3 of the array: arr[3]=13, *(pi+3)=13, (*pa)[3]=13 Element 4 of the array: arr[4]=14, *(pi+4)=14, (*pa)[4]=14 |
This will yield the following assembly code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
[johndoe@ArchLinux]% radare2 Pointer_to_Array [0x004004d7]> aaa [x] Analyze all flags starting with sym. and entry0 (aa) [x] Analyze len bytes of instructions for references (aar) [x] Analyze function calls (aac) [ ] [*] Use -AA or aaaa to perform additional experimental analysis. [x] Constructing a function name for fcn.* and sym.func.* functions (aan)) [0x004004d7]> s sym.main [0x004004d7]> afvn local_8h arr[2] [0x004004d7]> afvn local_ch arr[3] [0x004004d7]> afvn local_10h arr[4] [0x004004d7]> pdf ;-- main: / (fcn) sym.main 104 | sym.main (); | ; var int arr[1] @ rsp+0x4 | ; var int arr[2] @ rsp+0x8 | ; var int arr[3] @ rsp+0xc | ; var int arr[4] @ rsp+0x10 | ; DATA XREF from 0x0040041d (entry0) | 0x004004d7 53 push rbx ; Pointer_to_Array.c:3 int main(int argc, char* argv[]){ | 0x004004d8 4883ec20 sub rsp, 0x20 | 0x004004dc c704240a0000. mov dword [rsp], 0xa ; Pointer_to_Array.c:5 int arr[] = {10,11,12,13,14}; | 0x004004e3 c74424040b00. mov dword [arr[1]], 0xb ; [0xb:4]=0 | 0x004004eb c74424080c00. mov dword [arr[2]], 0xc ; [0xc:4]=0 | 0x004004f3 c744240c0d00. mov dword [arr[3]], 0xd ; [0xd:4]=0x2000000 | 0x004004fb c74424100e00. mov dword [arr[4]], 0xe ; [0xe:4]=0x20000 | 0x00400503 bb00000000 mov ebx, 0 | ; JMP XREF from 0x00400532 (sym.main) | .-> 0x00400508 8b0c9c mov ecx, dword [rsp + rbx*4] ; Pointer_to_Array.c:18 printf("Element %d of the array: arr[%d]=%d, *(pi+%d)=%d, (*pa)[%d]=%d\n", i, i, arr[i], i, *(pi+i), i, (*pa)[i]); | | 0x0040050b 51 push rcx | | 0x0040050c 53 push rbx | | 0x0040050d 4189c9 mov r9d, ecx | | 0x00400510 4189d8 mov r8d, ebx | | 0x00400513 89da mov edx, ebx | | 0x00400515 89de mov esi, ebx | | 0x00400517 bfc8054000 mov edi, str.Element__d_of_the_array:_arr__d___d____pi__d___d____pa___d___d_n ; "Element %d of the array: arr[%d]=%d, *(pi+%d)=%d, (*pa)[%d]=%d." @ 0x4005c8 | | 0x0040051c b800000000 mov eax, 0 | | 0x00400521 e8cafeffff call sym.imp.printf ; int printf(const char *format) | | 0x00400526 4883c301 add rbx, 1 | | 0x0040052a 4883c410 add rsp, 0x10 ; Pointer_to_Array.c:17 for(int i = 0; i < N; i++) { | | 0x0040052e 4883fb05 cmp rbx, 5 | `=< 0x00400532 75d4 jne 0x400508 | 0x00400534 b800000000 mov eax, 0 ; Pointer_to_Array.c:21 } | 0x00400539 4883c420 add rsp, 0x20 | 0x0040053d 5b pop rbx \ 0x0040053e c3 ret [0x004004d7]> |
That's amazing, there is no stack frame initialized, unnecessary pointer calculations are omitted, variable "i" is stored in register RBX and there is just a single method to access array elements.