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

../../../../../../_images/mult_int_Fortran_ARM64_splash1.png

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 value 2

  • It’s stored at offset +12 on the stack (this becomes variable a)


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 at sp+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 into w1, b into w0

  • Multiply a * b, result in w0

  • Store result into c at sp+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 sp)

Variable

Size

Description

+12

a

4B

Integer a = 2

+8

b

4B

Integer b = 3

+4

c

4B

a * b = 6


Summary#

This is a minimal Fortran program compiled to AArch64 that:

  1. Initializes two integers a=2, b=3

  2. Computes c = a * b

  3. Stores the result on the stack

  4. Performs no I/O

  5. 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 Fortran program mult. Fortran compilers often mangle the main program name to MAIN__ (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 for a, 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 the program 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 for a = 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 to a, 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 prepares b = 3.

  • 10: b9000be0 str w0, [sp, #8]
    Store w0 (3) into [sp + 8]. This assigns to b (next stack slot below a).

  • 14: b9400fe1 ldr w1, [sp, #12]
    Load the value from [sp + 12] ( a = 2) into w1. This loads the first operand for the multiplication c = a * b.

  • 18: b9400be0 ldr w0, [sp, #8]
    Load [sp + 8] ( b = 3) into w0. Now w0 holds b, w1 holds a.

  • 1c: 1b007c20 mul w0, w1, w0
    Multiply w1 ( a ) by w0 ( b ), storing the result in w0. This computes 2 * 3 = 6, corresponding to c = a * b. The mul 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 to c (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 to end 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 via R_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 offset ffffe9 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!