Fortran - ARM 64-Bit Platform - Multiply Two Integers#

Introduction#
In this section we will be disassembling simple binaries generated by the Fortran high-level language compiled for the 64-bit ARM platform.
Project code for this section is contained in my markkhusid/Disassembling-Binaries.
The program mult_int.f08#
program mult
implicit none
integer :: a, b, c
a = 2
b = 3
c = a * b
end program mult
The program displays the contents of the program mult_int.f08. The program creates three integers: a, b, and c. a
is assigned the value of 2, b
is assigned the value of 3, and c
is assigned the result of the operation a * b
.
The program is obviously very simple, with no inputs and outputs. The idea is to generate the binary and look at the disassembly to learn about the workings of the 64-bit ARM processor platform.
The chosen test system is my trusty Samsung Chromebook Plus V2, which has a 64-bit ARMv8 processor. This is a very convenient platform for this exercise due to its availability and ease of access from multiple remote systems via SSH.
The program is compiled with the GNU Fortran compiler, gfortran
, which is available on the ARM platform. The program is compiled with debugging information using the -ggdb3
option, which allows us to debug the program in GDB and see the source code interspersed with the assembly instructions.
The program is compiled with:
$ gfortran -ggdb3 mult_int.f08 -o mult_int_Fortran_aarch64_ggdb3
For general edification, we also have gfortran
produce generic assembly with the -S
option, an object file with the -o
option, and object dumps of the object and executable files.
The generic assembly is generated by using the -S
(assembly) option:
$ gfortran -S -ggdb3 mult_int.f08 -o mult_int.s
The object file is generated by using the -c
(compile) option:
$ gfortran -c -ggdb3 mult_int.s -o mult_int.o
The objdump files are generated by using the following command and options:
$ objdump -x -D -S -s -g -t mult_int.o > objdump_of_dot_o.txt
$ objdump -x -D -S -s -g -t mult_int_Fortran_aarch64_ggdb3 > objdump_of_dot_exe.txt
A rundown of the objdump options is shown here:
$objdump
Usage: objdump <option(s)> <file(s)>
Display information from object <file(s)>.
At least one of the following switches must be given:
-a, --archive-headers Display archive header information
-f, --file-headers Display the contents of the overall file header
-p, --private-headers Display object format specific file header contents
-P, --private=OPT,OPT... Display object format specific contents
-h, --[section-]headers Display the contents of the section headers
-x, --all-headers Display the contents of all headers
-d, --disassemble Display assembler contents of executable sections
-D, --disassemble-all Display assembler contents of all sections
--disassemble=<sym> Display assembler contents from <sym>
-S, --source Intermix source code with disassembly
--source-comment[=<txt>] Prefix lines of source code with <txt>
-s, --full-contents Display the full contents of all sections requested
-g, --debugging Display debug information in object file
-e, --debugging-tags Display debug information using ctags style
-G, --stabs Display (in raw form) any STABS info in the file
-W, --dwarf[a/=abbrev, A/=addr, r/=aranges, c/=cu_index, L/=decodedline,
f/=frames, F/=frames-interp, g/=gdb_index, i/=info, o/=loc,
m/=macro, p/=pubnames, t/=pubtypes, R/=Ranges, l/=rawline,
s/=str, O/=str-offsets, u/=trace_abbrev, T/=trace_aranges,
U/=trace_info]
Display the contents of DWARF debug sections
-Wk,--dwarf=links Display the contents of sections that link to
separate debuginfo files
-WK,--dwarf=follow-links
Follow links to separate debug info files (default)
-WN,--dwarf=no-follow-links
Do not follow links to separate debug info files
-L, --process-links Display the contents of non-debug sections in
separate debuginfo files. (Implies -WK)
--ctf[=SECTION] Display CTF info from SECTION, (default `.ctf')
--sframe[=SECTION] Display SFrame info from SECTION, (default '.sframe')
-t, --syms Display the contents of the symbol table(s)
-T, --dynamic-syms Display the contents of the dynamic symbol table
-r, --reloc Display the relocation entries in the file
-R, --dynamic-reloc Display the dynamic relocation entries in the file
@<file> Read options from <file>
-v, --version Display this program's version number
-i, --info List object formats and architectures supported
-H, --help Display this information
In our case, we want -x
(all headers), -D
(disassemble all), -S
(display source code with assembly), -s
(full contents of all sections), -g
(debug info), and finally, -t
(display contents of the symbol tables).
We will now disassemble this program on the 64-bit ARM platform and step through the assembly instructions.
Disassembling mult_int_Fortran_aarch64_ggdb3#
When we look at the executable’s objdump, we notice that there are two functions of interest, one is main
, and the other is MAIN__
. The Fortran compiler sets up the program arguments and options in main
, while the actual program is contained within MAIN__
(that is capital MAIN followed by two underscores).
The following text from the executable’s objdump illustrates this:
Disassembly of section .text:
0000000000000000 <MAIN__>:
program mult
0: d10043ff sub sp, sp, #0x10
implicit none
integer :: a, b, c
a = 2
4: 52800040 mov w0, #0x2 // #2
8: b9000fe0 str w0, [sp, #12]
b = 3
c: 52800060 mov w0, #0x3 // #3
10: b9000be0 str w0, [sp, #8]
c = a * b
14: b9400fe1 ldr w1, [sp, #12]
18: b9400be0 ldr w0, [sp, #8]
1c: 1b007c20 mul w0, w1, w0
20: b90007e0 str w0, [sp, #4]
end program mult
24: d503201f nop
28: 910043ff add sp, sp, #0x10
2c: d65f03c0 ret
0000000000000030 <main>:
30: a9be7bfd stp x29, x30, [sp, #-32]!
34: 910003fd mov x29, sp
38: b9001fe0 str w0, [sp, #28]
3c: f9000be1 str x1, [sp, #16]
40: f9400be1 ldr x1, [sp, #16]
44: b9401fe0 ldr w0, [sp, #28]
48: 94000000 bl 0 <_gfortran_set_args>
48: R_AARCH64_CALL26 _gfortran_set_args
4c: 90000000 adrp x0, 0 <MAIN__>
4c: R_AARCH64_ADR_PREL_PG_HI21 .rodata
50: 91000001 add x1, x0, #0x0
50: R_AARCH64_ADD_ABS_LO12_NC .rodata
54: 528000e0 mov w0, #0x7 // #7
58: 94000000 bl 0 <_gfortran_set_options>
58: R_AARCH64_CALL26 _gfortran_set_options
5c: 97ffffe9 bl 0 <MAIN__>
60: 52800000 mov w0, #0x0 // #0
64: a8c27bfd ldp x29, x30, [sp], #32
68: d65f03c0 ret
Explaining the Dissassembly by ChatGPT 4o#
This is the AArch64 (ARM 64-bit) assembly output from a Fortran program, likely compiled with gfortran
, that performs a simple multiplication:
program mult
implicit none
integer :: a, b, c
a = 2
b = 3
c = a * b
end program mult
Let’s break down the assembly section .text
step-by-step:
Function: MAIN__
#
This is the Fortran-generated main subroutine (program mult
compiles to MAIN__
).
0000000000000000 <MAIN__>:
1. Prologue: Stack setup#
0: d10043ff sub sp, sp, #0x10
Allocate 16 bytes on the stack for local variables (
a
,b
,c
).Adjust the stack pointer (
sp
) downward to make room.
2. Initialize a = 2
#
4: 52800040 mov w0, #0x2 // w0 = 2
8: b9000fe0 str w0, [sp, #12] // store `a` at sp+12
w0
holds 32-bit immediate value2
It’s stored at offset
+12
on the stack (this becomes variablea
)
3. Initialize b = 3
#
c: 52800060 mov w0, #0x3 // w0 = 3
10: b9000be0 str w0, [sp, #8] // store `b` at sp+8
b
is stored atsp+8
4. Compute c = a * b
#
14: b9400fe1 ldr w1, [sp, #12] // w1 = a
18: b9400be0 ldr w0, [sp, #8] // w0 = b
1c: 1b007c20 mul w0, w1, w0 // w0 = w1 * w0 = a * b
20: b90007e0 str w0, [sp, #4] // store c at sp+4
Load
a
intow1
,b
intow0
Multiply
a * b
, result inw0
Store result into
c
atsp+4
5. Epilogue: Clean up stack#
24: d503201f nop // no-op
28: 910043ff add sp, sp, #0x10 // restore original stack pointer
2c: d65f03c0 ret // return from MAIN__
Stack pointer restored
Function returns
Function: main
#
This is the C main
function, generated by the Fortran runtime to bootstrap the program.
0000000000000030 <main>:
1. Prologue#
30: a9be7bfd stp x29, x30, [sp, #-32]!
34: 910003fd mov x29, sp
Save frame pointer and return address on the stack (standard procedure prologue)
Set up new frame pointer
2. Store argc, argv#
38: b9001fe0 str w0, [sp, #28] // store argc
3c: f9000be1 str x1, [sp, #16] // store argv
Save arguments to stack
3. Initialize Fortran runtime arguments#
40: f9400be1 ldr x1, [sp, #16] // load argv
44: b9401fe0 ldr w0, [sp, #28] // load argc
48: 94000000 bl 0 <_gfortran_set_args>
Calls Fortran runtime to set command-line args
The relocations indicate linker will patch real address of
_gfortran_set_args
4. Set Fortran runtime options#
4c: 90000000 adrp x0, 0 <MAIN__> // load base address of .rodata (page)
50: 91000001 add x1, x0, #0x0 // offset within page
54: 528000e0 mov w0, #0x7 // options mask?
58: 94000000 bl 0 <_gfortran_set_options>
Sets runtime options, likely to enable formatting or I/O behavior
5. Call Fortran main program#
5c: 97ffffe9 bl 0 <MAIN__>
Branch to
MAIN__
to execute the Fortran logic
6. Return from main#
60: 52800000 mov w0, #0x0 // return code 0
64: a8c27bfd ldp x29, x30, [sp], #32
68: d65f03c0 ret
Restore frame pointer and return address
Return from
main
Stack Layout Summary (inside MAIN__
)#
Offset (from |
Variable |
Size |
Description |
---|---|---|---|
|
|
4B |
Integer a = 2 |
|
|
4B |
Integer b = 3 |
|
|
4B |
a * b = 6 |
Summary#
This is a minimal Fortran program compiled to AArch64 that:
Initializes two integers
a=2
,b=3
Computes
c = a * b
Stores the result on the stack
Performs no I/O
Exits gracefully via a C-compatible
main
function
Let me know if you’d like the equivalent source in C or Fortran with comments.
Explaining the Disassembly by Grok 4#
Overview of the Disassembly#
This disassembly represents the machine code generated by compiling a simple Fortran program using a compiler like gfortran (from the GNU Fortran suite) targeting the AArch64 architecture (ARM64, commonly used in modern ARM processors like those in Apple Silicon or server ARM chips). The Fortran source code, as annotated in the disassembly, is a basic program that declares three integers (a
, b
, c
), assigns values to a
and b
, computes their product, and stores it in c
. It doesn’t output anything or perform I/O, so it’s essentially a no-op beyond the computation.
The disassembly is divided into two main functions:
<MAIN__>
: This is the compiled body of the Fortranprogram mult
. Fortran compilers often mangle the main program name toMAIN__
(in uppercase with underscores) to distinguish it from other subroutines.<main>
: This is the actual program entry point, written in a C-like style. It’s generated by the compiler to handle runtime initialization for Fortran (e.g., setting up command-line arguments and options via the GNU Fortran runtime library, libgfortran). It calls<MAIN__>
and then exits cleanly.
The code uses stack-based storage for variables (common in optimized compilations without register allocation for such simple locals). Instructions are in hexadecimal offsets, followed by the opcode, mnemonic, and operands. Some instructions have relocations (e.g., R_AARCH64_CALL26
), which are linker directives for resolving addresses at link time (e.g., calling external functions).
I’ll explain each section in detail, breaking down every instruction, its purpose, and how it relates to the Fortran source. Note that AArch64 uses 64-bit registers (x0-x31 for general-purpose, w0-w31 for 32-bit views), stack pointer (sp), and program counter (pc). Memory access is little-endian by default.
Detailed Explanation of <MAIN__>
(Offset 0x0000000000000000)#
This function corresponds directly to the Fortran program body. It allocates stack space for the three integers (a
, b
, c
), assigns values, performs the multiplication, and then deallocates the stack before returning. Since Fortran integers are typically 32-bit (4 bytes), each variable takes 4 bytes on the stack.
0: d10043ff sub sp, sp, #0x10
Subtract 0x10 (16 in decimal) from the stack pointer (sp). This allocates 16 bytes of stack space: 4 bytes each fora
,b
,c
(12 bytes total), plus 4 bytes of padding to maintain 16-byte stack alignment (AArch64 ABI requirement). The stack grows downward, so subtracting from sp creates space. This corresponds to entering theprogram mult
and preparing for variable declarations.4: 52800040 mov w0, #0x2
Move the immediate value 0x2 (2 in decimal) into the 32-bit register w0. This sets up the value fora = 2
. w0 is a temporary register here.8: b9000fe0 str w0, [sp, #12]
Store the value from w0 (2) into memory at [sp + 12]. This assigns toa
, which is at offset 12 from the current sp (top of the allocated stack space).c: 52800060 mov w0, #0x3
Move 0x3 (3) into w0. This preparesb = 3
.10: b9000be0 str w0, [sp, #8]
Store w0 (3) into [sp + 8]. This assigns tob
(next stack slot belowa
).14: b9400fe1 ldr w1, [sp, #12]
Load the value from [sp + 12] (a
= 2) into w1. This loads the first operand for the multiplicationc = a * b
.18: b9400be0 ldr w0, [sp, #8]
Load [sp + 8] (b
= 3) into w0. Now w0 holdsb
, w1 holdsa
.1c: 1b007c20 mul w0, w1, w0
Multiply w1 (a
) by w0 (b
), storing the result in w0. This computes 2 * 3 = 6, corresponding toc = a * b
. Themul
instruction handles signed/unsigned multiplication (integers are signed in Fortran, but for positive values it doesn’t matter).20: b90007e0 str w0, [sp, #4]
Store the result from w0 (6) into [sp + 4]. This assigns toc
(next stack slot).24: d503201f nop
No operation. This is a padding instruction, often inserted by compilers for alignment or to avoid hazards. It marks the end of the computation before cleanup, corresponding toend program mult
.28: 910043ff add sp, sp, #0x10
Add 0x10 back to sp, deallocating the stack space. This cleans up after the variables.2c: d65f03c0 ret
Return from the function by jumping to the address in x30 (link register, set by the caller). This exits<MAIN__>
.
In summary, <MAIN__>
is a straightforward translation: allocate stack, assign constants to a
and b
, load them, multiply, store to c
, deallocate, and return. No optimizations like register allocation for variables are applied here (possibly due to debug mode or simple code), so everything uses stack memory.
Detailed Explanation of <main>
(Offset 0x0000000000000030)#
This is the program’s true entry point, generated to bridge C-style startup (e.g., from the OS or loader) to Fortran. It saves registers, passes command-line arguments to the Fortran runtime, sets compiler options, calls <MAIN__>
, and exits with status 0. It uses the AArch64 System V ABI for calling conventions (arguments in x0-x7, stack for more).
30: a9be7bfd stp x29, x30, [sp, #-32]!
Store pair: Save frame pointer (x29) and link register (x30) to [sp - 32], then set sp = sp - 32. This allocates 32 bytes of stack and saves caller-saved registers (ABI requirement for functions). The!
indicates pre-indexed write-back.34: 910003fd mov x29, sp
Set x29 (frame pointer) to current sp. This establishes a frame pointer for debugging or unwinding.38: b9001fe0 str w0, [sp, #28]
Store w0 (argc, the number of command-line arguments, passed by the OS) to [sp + 28].3c: f9000be1 str x1, [sp, #16]
Store x1 (argv, pointer to argument strings) to [sp + 16]. This saves the command-line args for later use.40: f9400be1 ldr x1, [sp, #16]
Load argv back into x1 (preparing for function call).44: b9401fe0 ldr w0, [sp, #28]
Load argc into w0.48: 94000000 bl 0 <_gfortran_set_args>
Branch with link (call) to_gfortran_set_args
(a libgfortran function). This passes argc (w0) and argv (x1) to initialize Fortran’s argument handling. The address is a placeholder (0), resolved by the linker viaR_AARCH64_CALL26
(26-bit PC-relative call).4c: 90000000 adrp x0, 0 <MAIN__>
Address relative page: Load the high 21 bits of the address of the.rodata
section (read-only data) into x0. This prepares to pass options data. Relocation:R_AARCH64_ADR_PREL_PG_HI21
resolves the page-aligned address.50: 91000001 add x1, x0, #0x0
Add low 12 bits to x0, storing in x1. This completes the address of some options array in.rodata
. Relocation:R_AARCH64_ADD_ABS_LO12_NC
(no carry).54: 528000e0 mov w0, #0x7
Move 7 into w0. This is likely the number of options or a flags value for_gfortran_set_options
.58: 94000000 bl 0 <_gfortran_set_options>
Call_gfortran_set_options
with count/flags (w0) and options pointer (x1, per ABI; wait, actually x0 should be count, x1 options—wait, registers: mov set w0=7, but add set x1=address. Yes, call passes x0=unused?, but ABI: first arg x0, second x1. Wait, adrp/add set x0=page, x1=full addr, then mov w0=7 overwrites x0. So args: x0=7 (count), x1=options. Relocation:R_AARCH64_CALL26
.5c: 97ffffe9 bl 0 <MAIN__>
Call<MAIN__>
(PC-relative branch backward). The offsetffffe9
is -23 in two’s complement, but actually computed to reach offset 0.60: 52800000 mov w0, #0x0
Move 0 into w0. This sets the exit status (success).64: a8c27bfd ldp x29, x30, [sp], #32
Load pair: Restore x29 and x30 from [sp], then add 32 to sp (post-indexed). This cleans up the stack.68: d65f03c0 ret
Return to caller (e.g., OS or runtime), exiting the program with status in w0.
In essence, <main>
initializes the Fortran runtime with args and options, runs the program body, and exits. The relocations ensure external symbols (like libgfortran functions) are linked correctly.
Additional Notes#
Compilation Assumptions: This looks like unoptimized code (-O0), as variables are stack-stored and not optimized away (the computation is dead code since
c
isn’t used). In higher optimization, this might be elided entirely.Runtime Dependencies: Relies on libgfortran for
_gfortran_set_args
and_gfortran_set_options
, which handle things like backtrace options, floating-point settings, etc.Execution Flow: OS calls
<main>
, which sets up, calls<MAIN__>
, computes 2*3=6 (unused), returns, and exits.Potential Extensions: If you ran this, it would do nothing visible—no print. To verify, you’d need to add I/O in Fortran or debug with GDB.
This covers the disassembly exhaustively. If you have questions about specific instructions or want to simulate execution, let me know!