1.30 Structures
In C/C++, a structure (struct) is a group of variables packed together in one block, sitting next to each other in memory. These variables can be of different types (int, char, float, and so on). The struct gives a name to this bundle, and gives a name to each field so we can access it.
1.30.1 MSVC: SYSTEMTIME example
The author starts by taking the Win32 SYSTEMTIME structure, which describes time.
Listing 1.329: WinBase.h
typedef struct _SYSTEMTIME { WORD wYear; // year (e.g. 2026) WORD wMonth; // month (1–12) WORD wDayOfWeek; // day of week (0=Sunday, 6=Saturday) WORD wDay; // day of month (1–31) WORD wHour; // hour (0–23) WORD wMinute; // minute (0–59) WORD wSecond; // second (0–59) WORD wMilliseconds; // milliseconds (0–999)} SYSTEMTIME, *PSYSTEMTIME;Let's write a C function to get the current time:
#include <windows.h>#include <stdio.h>
void main(){ SYSTEMTIME t; // declare a SYSTEMTIME struct on the stack (16 bytes) GetSystemTime(&t); // fill the struct with current UTC time printf("%04d-%02d-%02d %02d:%02d:%02d\n", t.wYear, t.wMonth, t.wDay, t.wHour, t.wMinute, t.wSecond); return;}We get (MSVC 2010):
Listing 1.330: MSVC 2010 /GS-
_t$ = -16 ; size = 16 bytes ; SYSTEMTIME struct lives here on the stack_main PROC push ebp mov ebp, esp sub esp, 16 ; reserve 16 bytes on stack for the struct (8 WORDs × 2 bytes)
lea eax, DWORD PTR _t$[ebp] ; EAX = address of the struct (pointer to wYear = pointer to whole struct) push eax ; push pointer as argument to GetSystemTime call DWORD PTR __imp__GetSystemTime@4 ; call GetSystemTime(&t) — fills all 8 fields
movzx ecx, WORD PTR _t$[ebp+12] ; load wSecond (offset 12 from struct start) push ecx ; push as 7th argument to printf movzx edx, WORD PTR _t$[ebp+10] ; load wMinute (offset 10) push edx ; push as 6th argument movzx eax, WORD PTR _t$[ebp+8] ; load wHour (offset 8) push eax ; push as 5th argument movzx ecx, WORD PTR _t$[ebp+6] ; load wDay (offset 6) push ecx ; push as 4th argument movzx edx, WORD PTR _t$[ebp+2] ; load wMonth (offset 2) push edx ; push as 3rd argument movzx eax, WORD PTR _t$[ebp] ; load wYear (offset 0 — start of struct) push eax ; push as 2nd argument push OFFSET $SG78811 ; push format string as 1st argument call _printf ; call printf(format, year, month, day, hour, min, sec) add esp, 28 ; clean up stack (7 arguments × 4 bytes) xor eax, eax ; return 0 mov esp, ebp pop ebp ret 0_main ENDPLet me explain this step by step to make it as clear as possible.
As we said, the fields are stored in the order you declare them. Each field takes space according to its type. In our example:
* WORD = 2 bytes (16 bits)
* 8 fields × 2 bytes = 16 bytes total
Field layout in memory (offsets from the start of the struct):
| Field | Offset (bytes) |
|---|---|
wYear |
0 |
wMonth |
2 |
wDayOfWeek |
4 |
wDay |
6 |
wHour |
8 |
wMinute |
10 |
wSecond |
12 |
wMilliseconds |
14 |
The struct begins with the field wYear. We can say that a pointer to the SYSTEMTIME struct is passed to GetSystemTime(), but we could equally say that a pointer to the field wYear is passed — it is the same thing. GetSystemTime() writes the current year into the WORD pointer it receives, then advances 2 bytes forward, writes the current month, and so on.
x32dbg
Let's compile this example in MSVC with the options /GS- /MD and run it in x32dbg using this command:
cl /Od /Zi /GS- systemtime.c
Let's open the data and stack view at the address being passed as the first argument to GetSystemTime(), and wait until it executes. We see this:
The system time at the moment of execution on my machine is July 9, 2026, 20:59:51.
Listing 1.331: printf() output
And these are the values currently in memory:
| Hex | Decimal | Field |
|---|---|---|
0x07EA |
2026 | wYear |
0x0007 |
7 | wMonth |
0x0004 |
4 | wDayOfWeek |
0x0009 |
9 | wDay |
0x0014 |
20 | wHour |
0x002F |
47 | wMinute |
0x0035 |
53 | wSecond |
0x0209 |
521 | wMilliseconds |
The same values appear in the stack window, but grouped as 32-bit values. Then printf() simply takes the values it needs and prints them to the screen. Some values are not printed by printf() (such as wDayOfWeek and wMilliseconds), but they are sitting in memory right now, available for use.
Replacing the structure with array
The author pointed out that the fact that struct fields are simply variables sitting next to each other can be easily demonstrated by doing the following. Keeping the definition of SYSTEMTIME in mind, we can rewrite the simple example like this:
#include <windows.h>#include <stdio.h>
void main(){ WORD array[8]; // plain array of 8 WORDs — same memory layout as SYSTEMTIME GetSystemTime(array); // pass array as if it were a SYSTEMTIME pointer printf("%04d-%02d-%02d %02d:%02d:%02d\n", array[0] /* wYear */, array[1] /* wMonth */, array[3] /* wDay */, array[4] /* wHour */, array[5] /* wMinute */, array[6] /* wSecond */); return;}The compiler only issues a warning — just a reminder that you are doing something that is not type-safe:
systemtime2.c(7) : warning C4133: 'function' : incompatible types - from 'WORD [8]' to 'LPSYSTEMTIME'But it still generates this code:
Listing 1.332: Non-optimizing MSVC 2010
$SG78573 DB '%04d-%02d-%02d %02d:%02d:%02d', 0aH, 00H ; format string
_array$ = -16 ; size = 16 bytes ; the array lives here on the stack_main PROC push ebp mov ebp, esp sub esp, 16 ; allocate 16 bytes for the array (same as the struct)
lea eax, DWORD PTR _array$[ebp] ; EAX = base address of the array push eax ; pass as argument to GetSystemTime call DWORD PTR __imp__GetSystemTime@4 ; GetSystemTime fills array[0..7] with time fields
movzx ecx, WORD PTR _array$[ebp+12] ; array[6] = wSecond (offset 12) push ecx movzx edx, WORD PTR _array$[ebp+10] ; array[5] = wMinute (offset 10) push edx movzx eax, WORD PTR _array$[ebp+8] ; array[4] = wHour (offset 8) push eax movzx ecx, WORD PTR _array$[ebp+6] ; array[3] = wDay (offset 6) push ecx movzx edx, WORD PTR _array$[ebp+2] ; array[1] = wMonth (offset 2) push edx movzx eax, WORD PTR _array$[ebp] ; array[0] = wYear (offset 0) push eax push OFFSET $SG78573 ; format string call _printf add esp, 28 ; clean up stack xor eax, eax mov esp, ebp pop ebp ret 0_main ENDPIt does exactly the same work. In this code specifically, one cannot say with certainty whether a struct was defined or an array — they look identical at the assembly level.
This is also a reminder that a struct can be changed or replaced by developers over time, and the assembly output would still look the same.
1.30.2 Let's allocate space for a structure using malloc()
Sometimes it is easier to place structures not on the local stack, but on the heap.
Let's first quickly explain what the heap is.
The Heap is a region in a program's memory used for Dynamic Memory Allocation. That means when you need to reserve space in memory at runtime and you don't know its size until that moment, you use the Heap.
#include <windows.h>#include <stdio.h>
void main(){ SYSTEMTIME *t; t = (SYSTEMTIME *)malloc(sizeof(SYSTEMTIME)); // allocate a SYSTEMTIME-sized block on the heap GetSystemTime(t); // fill the struct with current system time printf("%04d-%02d-%02d %02d:%02d:%02d\n", t->wYear, t->wMonth, t->wDay, t->wHour, t->wMinute, t->wSecond); // print the time fields free(t); // release the heap memory return;}
Let's compile it now with optimization (/Ox) so it's easier to see what we need.
Listing 1.333: Optimizing MSVC
_main PROC push esi ; save old ESI value to restore later push 16 ; push 16 onto the stack as argument to malloc call _malloc ; call malloc(16) add esp, 4 ; adjust the stack after the call (remove the argument) mov esi, eax ; ESI = address of the new block (return value from malloc) push esi ; push the address as argument to GetSystemTime call DWORD PTR __imp__GetSystemTime@4 ; call GetSystemTime(esi) ; now ESI holds the address of the struct filled with data movzx eax, WORD PTR [esi+12] ; read wSecond (offset 12) and zero-extend to 32 bits movzx ecx, WORD PTR [esi+10] ; read wMinute (offset 10) movzx edx, WORD PTR [esi+8] ; read wHour (offset 8) push eax ; push wSecond movzx eax, WORD PTR [esi+6] ; read wDay (offset 6) push ecx ; push wMinute movzx ecx, WORD PTR [esi+2] ; read wMonth (offset 2) push edx ; push wHour movzx edx, WORD PTR [esi] ; read wYear (offset 0) push eax ; push wDay push ecx ; push wMonth push edx ; push wYear push OFFSET $SG78833 ; push format string pointer call _printf ; print push esi ; push address to free call _free ; release the memory add esp, 32 ; adjust stack after printf and free (all arguments) xor eax, eax ; eax = 0 (main return value) pop esi ; restore old ESI ret 0_main ENDP
Here we find sizeof(SYSTEMTIME) = 16, which is the exact number of bytes that will be allocated by malloc(). As for malloc(), it returns a pointer to the new memory block in register EAX, which then moves to register ESI. The Win32 function GetSystemTime() takes care of preserving the value in ESI.
(A small note about ESI: it is a non-volatile register in the Windows x86 calling convention. This means any function (like malloc, GetSystemTime, or printf) may use and modify it. So if we want to keep a value in it throughout the function, that is why it is not saved here and continues to be used after the GetSystemTime() call.)
There is a new instruction MOVZX (Move with Zero eXtend). It is used in most cases like MOVSX, but it sets the remaining bits to 0. This is because printf() needs a 32-bit int, but we have a WORD in the struct which is an unsigned 16-bit type. That is why when copying the value from a WORD to an int, bits 16 through 31 must be zeroed out, since they might contain random noise left over from previous operations on the registers.
In this example, we can represent the structure as an array of 8 WORDs:
#include <windows.h>#include <stdio.h>
void main(){ WORD *t; t = (WORD *)malloc(16); // allocate 16 bytes on the heap GetSystemTime(t); // fill with system time (treats buffer as SYSTEMTIME) printf("%04d-%02d-%02d %02d:%02d:%02d\n", t[0] /* wYear */, t[1] /* wMonth */, t[3] /* wDay */, t[4] /* wHour */, t[5] /* wMinute */, t[6] /* wSecond */); // access fields by index free(t); // release the memory return;}And this is what it produces:
Listing 1.334: Optimizing MSVC
$SG78594 DB '%04d-%02d-%02d %02d:%02d:%02d', 0aH, 00H ; format string with newline and null terminator_main PROC push esi push 16 ; push size argument to malloc call _malloc ; call malloc(16) add esp, 4 ; clean up stack mov esi, eax ; ESI = pointer to allocated block push esi ; push pointer as argument to GetSystemTime call DWORD PTR __imp__GetSystemTime@4 ; call GetSystemTime movzx eax, WORD PTR [esi+12] ; read wSecond (offset 12), zero-extend movzx ecx, WORD PTR [esi+10] ; read wMinute (offset 10), zero-extend movzx edx, WORD PTR [esi+8] ; read wHour (offset 8), zero-extend push eax ; push wSecond movzx eax, WORD PTR [esi+6] ; read wDay (offset 6), zero-extend push ecx ; push wMinute movzx ecx, WORD PTR [esi+2] ; read wMonth (offset 2), zero-extend push edx ; push wHour movzx edx, WORD PTR [esi] ; read wYear (offset 0), zero-extend push eax ; push wDay push ecx ; push wMonth push edx ; push wYear push OFFSET $SG78594 ; push format string call _printf ; call printf push esi ; push pointer to free call _free ; free the memory add esp, 32 ; clean up stack (all arguments) xor eax, eax ; return 0 pop esi ; restore ESI ret 0_main ENDP1.30.3 UNIX: struct tm
Let's take the tm struct from the time.h header in Linux as an example:
#include <stdio.h>#include <time.h>
void main(){ struct tm t; // declare a tm struct on the stack time_t unix_time; // variable to hold the current Unix timestamp
unix_time = time(NULL); // get current time as Unix timestamp localtime_r(&unix_time, &t); // convert Unix time to local time and fill the struct
printf("Year: %d\n", t.tm_year + 1900); // tm_year is years since 1900 printf("Month: %d\n", t.tm_mon); printf("Day: %d\n", t.tm_mday); printf("Hour: %d\n", t.tm_hour); printf("Minutes: %d\n", t.tm_min); printf("Seconds: %d\n", t.tm_sec);}Let's compile it in GCC 4.4.1:
Listing 1.335: GCC 4.4.1
main proc near push ebp mov ebp, esp and esp, 0FFFFFFF0h ; align stack to 16 bytes sub esp, 40h ; allocate local space mov dword ptr [esp], 0 ; first argument to time() = NULL call time mov [esp+3Ch], eax ; save the returned Unix timestamp lea eax, [esp+3Ch] ; get pointer to the returned time() value lea edx, [esp+10h] ; struct tm starts at ESP+10h mov [esp+4], edx ; pass pointer to start of struct as 2nd arg mov [esp], eax ; pass pointer to time result as 1st arg call localtime_r mov eax, [esp+24h] ; load tm_year lea edx, [eax+76Ch] ; edx = eax + 1900 (0x76C = 1900) mov eax, offset format ; "Year: %d\n" mov [esp+4], edx mov [esp], eax call printf mov edx, [esp+20h] ; load tm_mon mov eax, offset aMonthD ; "Month: %d\n" mov [esp+4], edx mov [esp], eax call printf mov edx, [esp+1Ch] ; load tm_mday mov eax, offset aDayD ; "Day: %d\n" mov [esp+4], edx mov [esp], eax call printf mov edx, [esp+18h] ; load tm_hour mov eax, offset aHourD ; "Hour: %d\n" mov [esp+4], edx mov [esp], eax call printf mov edx, [esp+14h] ; load tm_min mov eax, offset aMinutesD ; "Minutes: %d\n" mov [esp+4], edx mov [esp], eax call printf mov edx, [esp+10h] ; load tm_sec mov eax, offset aSecondsD ; "Seconds: %d\n" mov [esp+4], edx mov [esp], eax call printf leave retnmain endpSomehow, IDA did not assign names to the local variables on the local stack, but the author said we can figure it out without that information in this simple example.
Also pay attention to the instruction lea edx, [eax+76Ch] — this instruction simply adds 0x76C (1900) to the value in EAX, but without modifying any flags.
GDB
The author started by loading the example in GDB:
dennis@ubuntuvm:~/polygon$ dateMon Jun 2 18:10:37 EEST 2014
dennis@ubuntuvm:~/polygon$ gcc GCC_tm.c -o GCC_tm
dennis@ubuntuvm:~/polygon$ gdb GCC_tmGNU gdb (GDB) 7.6.1-ubuntu...Reading symbols from /home/dennis/polygon/GCC_tm...(no debugging symbols found)...done.
(gdb) b printfBreakpoint 1 at 0x8048330
(gdb) runStarting program: /home/dennis/polygon/GCC_tm
Breakpoint 1, __printf (format=0x80485c0 "Year: %d\n") at printf.c:2929 printf.c: No such file or directory.
(gdb) x/20x $esp0xbffff0dc: 0x080484c3 0x080485c0 0x000007de 0x000000000xbffff0ec: 0x08048301 0x538c93ed 0x00000025 0x0000000a0xbffff0fc: 0x00000012 0x00000002 0x00000005 0x000000720xbffff10c: 0x00000001 0x00000098 0x00000001 0x00002a300xbffff11c: 0x0804b090 0x08048530 0x00000000 0x00000000(gdb)
We could easily find our struct on the stack. First, let's see how it is defined in time.h:
Listing 1.337: time.h
struct tm{ int tm_sec; // seconds int tm_min; // minutes int tm_hour; // hours int tm_mday; // day of the month int tm_mon; // month int tm_year; // year (since 1900) int tm_wday; // day of the week int tm_yday; // day of the year int tm_isdst; // daylight saving time flag};
Note that here a 32-bit int is used instead of WORD as in SYSTEMTIME. So each field occupies 32 bits (4 bytes).
0xbffff0dc: 0x080484c3 0x080485c0 0x000007de 0x000000000xbffff0ec: 0x08048301 0x538c93ed 0x00000025 sec 0x0000000a min0xbffff0fc: 0x00000012 hour 0x00000002 mday 0x00000005 mon 0x00000072 year0xbffff10c: 0x00000001 wday 0x00000098 yday 0x00000001 isdst 0x00002a300xbffff11c: 0x0804b090 0x08048530 0x00000000 0x00000000Or as a table:
| Hex value | Decimal value | Field name |
|---|---|---|
| 0x00000025 | 37 | tm_sec |
| 0x0000000a | 10 | tm_min |
| 0x00000012 | 18 | tm_hour |
| 0x00000002 | 2 | tm_mday |
| 0x00000005 | 5 | tm_mon |
| 0x00000072 | 114 | tm_year |
| 0x00000001 | 1 | tm_wday |
| 0x00000098 | 152 | tm_yday |
| 0x00000001 | 1 | tm_isdst |
ARM
Optimizing Keil 6/2013 (Thumb mode)
We will use the same example.
Listing 1.338: Optimizing Keil 6/2013 (Thumb mode)
var_38 = -0x38var_34 = -0x34var_30 = -0x30var_2C = -0x2Cvar_28 = -0x28var_24 = -0x24timer = -0xC
PUSH {LR} ; save link registerMOVS R0, #0 ; argument to time() = NULLSUB SP, SP, #0x34 ; allocate local stack spaceBL time ; call time(NULL)STR R0, [SP,#0x38+timer] ; save returned Unix timestampMOV R1, SP ; R1 = pointer to struct tm (tp)ADD R0, SP, #0x38+timer ; R0 = pointer to timer variableBL localtime_r ; call localtime_r(&timer, tp)LDR R1, =0x76C ; R1 = 1900LDR R0, [SP,#0x38+var_24] ; load tm_yearADDS R1, R0, R1 ; R1 = tm_year + 1900ADR R0, aYearD ; "Year: %d\n"BL __2printf ; call printfLDR R1, [SP,#0x38+var_28] ; load tm_monADR R0, aMonthD ; "Month: %d\n"BL __2printf ; call printfLDR R1, [SP,#0x38+var_2C] ; load tm_mdayADR R0, aDayD ; "Day: %d\n"BL __2printf ; call printfLDR R1, [SP,#0x38+var_30] ; load tm_hourADR R0, aHourD ; "Hour: %d\n"BL __2printf ; call printfLDR R1, [SP,#0x38+var_34] ; load tm_minADR R0, aMinutesD ; "Minutes: %d\n"BL __2printf ; call printfLDR R1, [SP,#0x38+var_38] ; load tm_secADR R0, aSecondsD ; "Seconds: %d\n"BL __2printf ; call printfADD SP, SP, #0x34 ; deallocate local stack spacePOP {PC} ; returnOptimizing Xcode 4.6.3 (LLVM) (Thumb-2 mode)
IDA "knows" the tm struct (because IDA "knows" the argument types of library functions like localtime_r()), so it shows here the access to the struct's fields and their names.
Listing 1.339: Optimizing Xcode 4.6.3 (LLVM) (Thumb-2 mode)
var_38 = -0x38var_34 = -0x34
PUSH {R7,LR} ; save frame pointer and link registerMOV R7, SP ; set frame pointerSUB SP, SP, #0x30 ; allocate local stack spaceMOVS R0, #0 ; argument: time_t * = NULLBLX _time ; call time(NULL)ADD R1, SP, #0x38+var_34 ; R1 = pointer to struct tmSTR R0, [SP,#0x38+var_38] ; save returned timestampMOV R0, SP ; R0 = pointer to timestampBLX _localtime_r ; call localtime_rLDR R1, [SP,#0x38+var_34.tm_year] ; load tm_yearMOV R0, 0xF44 ; "Year: %d\n"ADD R0, PC ; resolve PC-relative addressADDW R1, R1, #0x76C ; R1 = tm_year + 1900BLX _printf ; call printfLDR R1, [SP,#0x38+var_34.tm_mon] ; load tm_monMOV R0, 0xF3A ; "Month: %d\n"ADD R0, PC ; resolve addressBLX _printf ; call printfLDR R1, [SP,#0x38+var_34.tm_mday] ; load tm_mdayMOV R0, 0xF35 ; "Day: %d\n"ADD R0, PC ; resolve addressBLX _printf ; call printfLDR R1, [SP,#0x38+var_34.tm_hour] ; load tm_hourMOV R0, 0xF2E ; "Hour: %d\n"ADD R0, PC ; resolve addressBLX _printf ; call printfLDR R1, [SP,#0x38+var_34.tm_min] ; load tm_minMOV R0, 0xF28 ; "Minutes: %d\n"ADD R0, PC ; resolve addressBLX _printf ; call printfLDR R1, [SP,#0x38+var_34] ; load tm_secMOV R0, 0xF25 ; "Seconds: %d\n"ADD R0, PC ; resolve addressBLX _printf ; call printfADD SP, SP, #0x30 ; deallocate stack spacePOP {R7,PC} ; restore frame pointer and return...00000000 tm struc ; (sizeof=0x2C, standard type)00000000 tm_sec DCD ? ; seconds field (4 bytes)00000004 tm_min DCD ? ; minutes field (4 bytes)00000008 tm_hour DCD ? ; hours field (4 bytes)0000000C tm_mday DCD ? ; day of month field (4 bytes)00000010 tm_mon DCD ? ; month field (4 bytes)00000014 tm_year DCD ? ; year field (4 bytes)00000018 tm_wday DCD ? ; day of week field (4 bytes)0000001C tm_yday DCD ? ; day of year field (4 bytes)00000020 tm_isdst DCD ? ; daylight saving time flag (4 bytes)00000024 tm_gmtoff DCD ? ; seconds east of UTC (4 bytes)00000028 tm_zone DCD ? ; timezone abbreviation pointer (offset)0000002C tm endsMIPS
Listing 1.340: Optimizing GCC 4.4.5 (IDA)
main:
; IDA does not know the struct field names, we named them manually:
var_40 = -0x40var_38 = -0x38seconds = -0x34minutes = -0x30hour = -0x2Cday = -0x28month = -0x24year = -0x20var_4 = -4
lui $gp, (__gnu_local_gp >> 16) ; load upper 16 bits of global pointeraddiu $sp, -0x50 ; allocate 80 bytes on stackla $gp, (__gnu_local_gp & 0xFFFF) ; load lower 16 bits of global pointersw $ra, 0x50+var_4($sp) ; save return addresssw $gp, 0x50+var_40($sp) ; save global pointerlw $t9, (time & 0xFFFF)($gp) ; load address of time() from GOTor $at, $zero ; load delay slot, NOPjalr $t9 ; call time(NULL)move $a0, $zero ; branch delay slot: pass NULL as argumentlw $gp, 0x50+var_40($sp) ; restore global pointer after calladdiu $a0, $sp, 0x50+var_38 ; A0 = pointer to unix_time variablelw $t9, (localtime_r & 0xFFFF)($gp) ; load address of localtime_r() from GOTaddiu $a1, $sp, 0x50+seconds ; A1 = pointer to start of struct tm (seconds field)jalr $t9 ; call localtime_r(&unix_time, &struct)sw $v0, 0x50+var_38($sp) ; branch delay slot: save time() resultlw $gp, 0x50+var_40($sp) ; restore global pointerlw $a1, 0x50+year($sp) ; load tm_yearlw $t9, (printf & 0xFFFF)($gp) ; load address of printf from GOTla $a0, $LC0 ; "Year: %d\n"jalr $t9 ; call printfaddiu $a1, 1900 ; branch delay slot: add 1900 to year (executes BEFORE jalr returns)lw $gp, 0x50+var_40($sp) ; restore global pointerlw $a1, 0x50+month($sp) ; load tm_monlw $t9, (printf & 0xFFFF)($gp) ; load address of printf from GOTlui $a0, ($LC1 >> 16) ; "Month: %d\n" (upper 16 bits)jalr $t9 ; call printfla $a0, ($LC1 & 0xFFFF) ; "Month: %d\n" (lower 16 bits) — branch delay slotlw $gp, 0x50+var_40($sp) ; restore global pointerlw $a1, 0x50+day($sp) ; load tm_mdaylw $t9, (printf & 0xFFFF)($gp) ; load address of printf from GOTlui $a0, ($LC2 >> 16) ; "Day: %d\n" (upper 16 bits)jalr $t9 ; call printfla $a0, ($LC2 & 0xFFFF) ; "Day: %d\n" (lower 16 bits) — branch delay slotlw $gp, 0x50+var_40($sp) ; restore global pointerlw $a1, 0x50+hour($sp) ; load tm_hourlw $t9, (printf & 0xFFFF)($gp) ; load address of printf from GOTlui $a0, ($LC3 >> 16) ; "Hour: %d\n" (upper 16 bits)jalr $t9 ; call printfla $a0, ($LC3 & 0xFFFF) ; "Hour: %d\n" (lower 16 bits) — branch delay slotlw $gp, 0x50+var_40($sp) ; restore global pointerlw $a1, 0x50+minutes($sp) ; load tm_minlw $t9, (printf & 0xFFFF)($gp) ; load address of printf from GOTlui $a0, ($LC4 >> 16) ; "Minutes: %d\n" (upper 16 bits)jalr $t9 ; call printfla $a0, ($LC4 & 0xFFFF) ; "Minutes: %d\n" (lower 16 bits) — branch delay slotlw $gp, 0x50+var_40($sp) ; restore global pointerlw $a1, 0x50+seconds($sp) ; load tm_seclw $t9, (printf & 0xFFFF)($gp) ; load address of printf from GOTlui $a0, ($LC5 >> 16) ; "Seconds: %d\n" (upper 16 bits)jalr $t9 ; call printfla $a0, ($LC5 & 0xFFFF) ; "Seconds: %d\n" (lower 16 bits) — branch delay slotlw $ra, 0x50+var_4($sp) ; restore return addressor $at, $zero ; load delay slot, NOPjr $ra ; returnaddiu $sp, 0x50 ; delay slot: deallocate stack
$LC0: .ascii "Year: %d\n"<0>$LC1: .ascii "Month: %d\n"<0>$LC2: .ascii "Day: %d\n"<0>$LC3: .ascii "Hour: %d\n"<0>$LC4: .ascii "Minutes: %d\n"<0>$LC5: .ascii "Seconds: %d\n"<0>
The author said this is an example where branch delay slots can confuse us. For example, there is the instruction addiu $a1, 1900 on line 35 which adds 1900 to the year number. It executes before the corresponding JALR on line 34 — don't forget this detail.
Structure as a set of values
To demonstrate that a struct is simply variables placed next to each other in one place, let's rewrite our example while looking at the tm struct definition one more time:
#include <stdio.h>#include <time.h>
void main(){ // declare all tm fields as individual local variables instead of a struct int tm_sec, tm_min, tm_hour, tm_mday, tm_mon, tm_year, tm_wday, tm_yday, tm_isdst; time_t unix_time;
unix_time = time(NULL); // get current Unix timestamp localtime_r(&unix_time, &tm_sec); // pass pointer to tm_sec as if it were the start of a struct
printf("Year: %d\n", tm_year + 1900); printf("Month: %d\n", tm_mon); printf("Day: %d\n", tm_mday); printf("Hour: %d\n", tm_hour); printf("Minutes: %d\n", tm_min); printf("Seconds: %d\n", tm_sec);}
Note: the pointer to the tm_sec field is what gets passed to localtime_r, i.e. to the first element of the "struct".
The compiler warns us:
GCC_tm2.c: In function 'main':GCC_tm2.c:11:5: warning: passing argument 2 of 'localtime_r' from incompatible pointer type [enabled by default]In file included from GCC_tm2.c:2:0:/usr/include/time.h:59:12: note: expected 'struct tm *' but argument is of type 'int *'But despite that, it generates this code:
main proc nearvar_30 = dword ptr -30hvar_2C = dword ptr -2Chunix_time = dword ptr -1Chtm_sec = dword ptr -18htm_min = dword ptr -14htm_hour = dword ptr -10htm_mday = dword ptr -0Chtm_mon = dword ptr -8tm_year = dword ptr -4
push ebpmov ebp, espand esp, 0FFFFFFF0h ; align stack to 16 bytessub esp, 30h ; allocate local spacecall __mainmov [esp+30h+var_30], 0 ; arg 0 (NULL) for time()call timemov [esp+30h+unix_time], eax ; save Unix timestamplea eax, [esp+30h+tm_sec] ; get pointer to tm_sec (start of "struct")mov [esp+30h+var_2C], eax ; pass as 2nd arg to localtime_rlea eax, [esp+30h+unix_time] ; get pointer to unix_timemov [esp+30h+var_30], eax ; pass as 1st arg to localtime_rcall localtime_rmov eax, [esp+30h+tm_year] ; load tm_yearadd eax, 1900 ; add 1900mov [esp+30h+var_2C], eax ; pass as value to printfmov [esp+30h+var_30], offset aYearD ; "Year: %d\n"call printfmov eax, [esp+30h+tm_mon] ; load tm_monmov [esp+30h+var_2C], eaxmov [esp+30h+var_30], offset aMonthD ; "Month: %d\n"call printfmov eax, [esp+30h+tm_mday] ; load tm_mdaymov [esp+30h+var_2C], eaxmov [esp+30h+var_30], offset aDayD ; "Day: %d\n"call printfmov eax, [esp+30h+tm_hour] ; load tm_hourmov [esp+30h+var_2C], eaxmov [esp+30h+var_30], offset aHourD ; "Hour: %d\n"call printfmov eax, [esp+30h+tm_min] ; load tm_minmov [esp+30h+var_2C], eaxmov [esp+30h+var_30], offset aMinutesD ; "Minutes: %d\n"call printfmov eax, [esp+30h+tm_sec] ; load tm_secmov [esp+30h+var_2C], eaxmov [esp+30h+var_30], offset aSecondsD ; "Seconds: %d\n"call printfleaveretnmain endpThis code is identical to what we saw before, and we cannot tell whether the original source code had a struct or just a bunch of variables.
And this code works. However, it is not recommended to do this in practice.
Usually, non-optimizing compilers allocate variables on the local stack in the same order they were declared in the function. However, there is no guarantee of this.
By the way, some other compilers might warn about the variables tm_year, tm_mon, tm_mday, tm_hour, tm_min being used without being initialized. Indeed, the compiler does not know that these fields will be filled by the localtime_r() function.
We chose this example because all the struct fields are of type int.
This would not work if the struct fields were 16-bit (WORD), as in the case of the SYSTEMTIME struct — GetSystemTime() would fill them incorrectly (because local variables are aligned on a 32-bit boundary).
So, a struct is simply a bundle of variables existing in one place, next to each other. We can say that a struct is an instruction to the compiler, directing it to keep the variables together in one place. By the way, in some very old versions of C (before 1972), there were no structures at all.
Structure as an array of 32-bit words
#include <stdio.h>#include <time.h>
void main(){ struct tm t; // declare a tm struct time_t unix_time; // Unix timestamp variable int i; // loop counter
unix_time = time(NULL); // get current Unix time localtime_r(&unix_time, &t); // fill the struct with local time
for (i = 0; i < 9; i++) // iterate over all 9 fields of the struct { int tmp = ((int*)&t)[i]; // cast the struct pointer to int* and access field i printf("0x%08X (%d)\n", tmp, tmp); // print the field value in hex and decimal }}This is what came out:
0x0000002D (45)0x00000033 (51)0x00000017 (23)0x0000001A (26)0x00000006 (6)0x00000072 (114)0x00000006 (6)0x000000CE (206)0x00000001 (1)The variables here are in the same order they were declared in the structure definition.
And this is what it looks like after compiling:
main proc near push ebp mov ebp, esp push esi push ebx and esp, 0FFFFFFF0h ; align stack to 16 bytes sub esp, 40h ; allocate local stack space mov dword ptr [esp], 0 ; timer = NULL (argument to time()) lea ebx, [esp+14h] ; EBX = pointer to start of struct tm call _time lea esi, [esp+38h] ; ESI = pointer to end of struct tm mov [esp+4], ebx ; tp (pointer to struct, 2nd arg to localtime_r) mov [esp+10h], eax ; save returned Unix timestamp lea eax, [esp+10h] mov [esp], eax ; timer (1st arg to localtime_r) call _localtime_r nop lea esi, [esi+0] ; NOP (padding for alignment)loc_80483D8: ; EBX here is a pointer to the start of the struct, ; ESI is a pointer to its end. mov eax, [ebx] ; load a 32-bit word from the array add ebx, 4 ; advance to the next field in the struct mov dword ptr [esp+4], offset a0x08xD ; "0x%08X (%d)\n" mov dword ptr [esp], 1 mov [esp+0Ch], eax ; pass value to printf() (decimal) mov [esp+8], eax ; pass value to printf() (hex) call ___printf_chk cmp ebx, esi ; have we reached the end of the struct? jnz short loc_80483D8 ; no — load the next value lea esp, [ebp-8] pop ebx pop esi pop ebp retnmain endpWe indeed found that the space on the local stack is treated first as a struct, and then as an array.
It is also possible to modify the struct fields through this pointer. And again, this approach is somewhat hackish and is not recommended for use in production code.
Structure as an array of bytes
We can go even further. Let's cast the pointer to an array of bytes and dump it:
#include <stdio.h>#include <time.h>
void main(){ struct tm t; // declare a tm struct time_t unix_time; // Unix timestamp variable int i, j; // loop counters
unix_time = time(NULL); // get current Unix time localtime_r(&unix_time, &t); // fill the struct with local time
for (i = 0; i < 9; i++) // iterate over all 9 fields (each 4 bytes) { for (j = 0; j < 4; j++) // iterate over each byte within the field printf("0x%02X ", ((unsigned char*)&t)[i * 4 + j]); // print byte in hex printf("\n"); // newline after each field }}Output:
0x2D 0x00 0x00 0x000x33 0x00 0x00 0x000x17 0x00 0x00 0x000x1A 0x00 0x00 0x000x06 0x00 0x00 0x000x72 0x00 0x00 0x000x06 0x00 0x00 0x000xCE 0x00 0x00 0x000x01 0x00 0x00 0x00The least significant byte comes first, because this is a little-endian architecture.
main proc near push ebp mov ebp, esp push edi push esi push ebx and esp, 0FFFFFFF0h ; align stack to 16 bytes sub esp, 40h ; allocate local stack space mov dword ptr [esp], 0 ; timer = NULL (argument to time()) lea esi, [esp+14h] ; ESI = pointer to start of struct tm call _time lea edi, [esp+38h] ; EDI = pointer to end of struct mov [esp+4], esi ; tp (pointer to struct, 2nd arg to localtime_r) mov [esp+10h], eax ; save returned Unix timestamp lea eax, [esp+10h] mov [esp], eax ; timer (1st arg to localtime_r) call _localtime_r lea esi, [esi+0] ; NOP (padding for alignment) ; ESI here is a pointer to the struct on the local stack. ; EDI is a pointer to the end of the struct.loc_8048408: xor ebx, ebx ; j = 0loc_804840A: movzx eax, byte ptr [esi+ebx] ; load a single byte from the struct add ebx, 1 ; j = j + 1 mov dword ptr [esp+4], offset a0x02x ; "0x%02X " mov dword ptr [esp], 1 mov [esp+8], eax ; pass the loaded byte to printf() call ___printf_chk cmp ebx, 4 ; have we printed all 4 bytes of this field? jnz short loc_804840A ; no — print next byte ; print a newline character (LF) mov dword ptr [esp], 0Ah ; c = '\n' add esi, 4 ; advance ESI to the next field (4 bytes) call _putchar cmp esi, edi ; have we reached the end of the struct? jnz short loc_8048408 ; no — reset j = 0 and process next field lea esp, [ebp-0Ch] pop ebx pop esi pop edi pop ebp retnmain endpGNU Scientific Library: Representation of complex numbers
The author explains this well here — this is a relatively rare case where an array is used intentionally instead of a struct:
Complex numbers are represented using the type gsl_complex. The internal representation of this type may differ across platforms and must not be accessed directly. The functions and macros described below allow manipulation of complex numbers in a portable way.
For reference, the default format of the gsl_complex type is illustrated by this struct:
typedef struct{ double dat[2]; // dat[0] = real part, dat[1] = imaginary part} gsl_complex;
The real part and the imaginary part are stored in adjacent elements of a two-element array. This eliminates any padding between the real and imaginary parts, dat[0] and dat[1], which allows the struct to map correctly onto packed complex arrays.
If this article helped you, please share it with others!
Some information may be outdated





