Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Fortran - ARM 64-Bit Platform - Modulo 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 https://github.com/markkhusid/Disassembling-Binaries/tree/master/Fortran/ARM_architecture/ARM64.


The program mod_int.f08

mod_int.f08
program mod_int

        implicit none

        integer         :: a, b, c

        a = 10
        b = 4

        c = MOD(a, b)

end program mod_int

The program displays the contents of the program mod_int.f08. The program creates three integers: a, b, and c. a is assigned the value of 10, b is assigned the value of 4, and c is assigned the result of the operation MOD(a, b). MOD() is a Fortran intrinsic function that returns the remainder of the division of a by 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 mod_int.f08 -o mod_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 mod_int.f08 -o mod_int.s

The object file is generated by using the -c (compile) option:

$ gfortran -c -ggdb3 mod_int.s -o mod_int.o

The objdump files are generated by using the following command and options:

$ objdump -x -D -S -s -g -t mod_int.o > objdump_of_dot_o.txt
$ objdump -x -D -S -s -g -t mod_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 mod_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:

0000000000000814 <MAIN__>:
program mod_int
 814:	d10043ff 	sub	sp, sp, #0x10

        implicit none

        integer         :: a, b, c

        a = 10
 818:	52800140 	mov	w0, #0xa                   	// #10
 81c:	b9000fe0 	str	w0, [sp, #12]
        b = 4
 820:	52800080 	mov	w0, #0x4                   	// #4
 824:	b9000be0 	str	w0, [sp, #8]

        c = MOD(a, b)
 828:	b9400fe0 	ldr	w0, [sp, #12]
 82c:	b9400be1 	ldr	w1, [sp, #8]
 830:	1ac10c02 	sdiv	w2, w0, w1
 834:	b9400be1 	ldr	w1, [sp, #8]
 838:	1b017c41 	mul	w1, w2, w1
 83c:	4b010000 	sub	w0, w0, w1
 840:	b90007e0 	str	w0, [sp, #4]

end program mod_int
 844:	d503201f 	nop
 848:	910043ff 	add	sp, sp, #0x10
 84c:	d65f03c0 	ret

0000000000000850 <main>:
 850:	a9be7bfd 	stp	x29, x30, [sp, #-32]!
 854:	910003fd 	mov	x29, sp
 858:	b9001fe0 	str	w0, [sp, #28]
 85c:	f9000be1 	str	x1, [sp, #16]
 860:	f9400be1 	ldr	x1, [sp, #16]
 864:	b9401fe0 	ldr	w0, [sp, #28]
 868:	97ffffa2 	bl	6f0 <_gfortran_set_args@plt>
 86c:	90000000 	adrp	x0, 0 <_init-0x660>
 870:	9124c001 	add	x1, x0, #0x930
 874:	528000e0 	mov	w0, #0x7                   	// #7
 878:	97ffff9a 	bl	6e0 <_gfortran_set_options@plt>
 87c:	97ffffe6 	bl	814 <MAIN__>
 880:	52800000 	mov	w0, #0x0                   	// #0
 884:	a8c27bfd 	ldp	x29, x30, [sp], #32
 888:	d65f03c0 	ret
 88c:	d503201f 	nop

Explaining the Dissassembly by ChatGPT 4o

This disassembly represents the ARM AArch64 machine code for a Fortran program that performs an integer modulus operation using MOD(a, b). In AArch64, there is no single instruction for modulo, so the operation must be implemented using multiple primitive instructions.

Let’s examine in great detail why and how the compiler synthesizes MOD(a, b) using multiple instructions.


High-Level Fortran Code

program mod_int
  implicit none
  integer :: a, b, c

  a = 10
  b = 4
  c = MOD(a, b)
end program mod_int

Disassembly Overview

0000000000000814 <MAIN__>:

This is the Fortran-compiled MAIN__ subroutine. Let’s go through it line by line, especially focusing on the MOD operation.


1. Stack Frame Setup

814:	d10043ff 	sub	sp, sp, #0x10

2. Initialize a = 10

818:	52800140 	mov	w0, #0xa           // w0 = 10
81c:	b9000fe0 	str	w0, [sp, #12]      // store a at sp+12

3. Initialize b = 4

820:	52800080 	mov	w0, #0x4           // w0 = 4
824:	b9000be0 	str	w0, [sp, #8]       // store b at sp+8

4. Perform c = MOD(a, b)

Here is the key section of interest:

828:	b9400fe0 	ldr	w0, [sp, #12]      // load a into w0
82c:	b9400be1 	ldr	w1, [sp, #8]       // load b into w1
830:	1ac10c02 	sdiv	w2, w0, w1         // w2 = a / b (integer division)
834:	b9400be1 	ldr	w1, [sp, #8]       // reload b into w1
838:	1b017c41 	mul	w1, w2, w1         // w1 = (a / b) * b
83c:	4b010000 	sub	w0, w0, w1         // w0 = a - (a / b) * b = MOD(a, b)
840:	b90007e0 	str	w0, [sp, #4]       // store result in c

Step-by-step Breakdown of MOD(a, b):

The formula implemented is:

MOD(a,b)=a(ab×b)\text{MOD}(a, b) = a - \left(\left\lfloor \frac{a}{b} \right\rfloor \times b\right)

In ARM64:


Why multiple operations?

There’s no native mod instruction

Unlike some architectures (e.g., x86 with idiv producing remainder), ARM AArch64 has only sdiv, which gives you:

To compute the modulus, we must do:

MOD(a,b)=a((a div b)b)\text{MOD}(a, b) = a - \left(\left( \text{a div b} \right) \cdot b\right)

This is mathematically sound because:

a=(a÷b)b+(amodb)amodb=a(a÷b)ba = (a \div b) \cdot b + (a \mod b) \Rightarrow a \mod b = a - (a \div b) \cdot b

Instruction-by-instruction: MOD(a, b)

Load values:

828:	b9400fe0 	ldr	w0, [sp, #12]      // w0 = a = 10
82c:	b9400be1 	ldr	w1, [sp, #8]       // w1 = b = 4

Integer Division:

830:	1ac10c02 	sdiv	w2, w0, w1         // w2 = w0 / w1 = 10 / 4 = 2

Reload b:

834:	b9400be1 	ldr	w1, [sp, #8]       // w1 = b = 4

Multiply quotient × b:

838:	1b017c41 	mul	w1, w2, w1         // w1 = 2 * 4 = 8

Subtract to get modulus:

83c:	4b010000 	sub	w0, w0, w1         // w0 = 10 - 8 = 2

Store result:

840:	b90007e0 	str	w0, [sp, #4]       // store c = 2

Function Epilogue

844:	d503201f 	nop
848:	910043ff 	add	sp, sp, #0x10      // restore original stack pointer
84c:	d65f03c0 	ret                   // return

Final Stack Layout

OffsetVariableValue
+12a10
+8b4
+4c2

Summary: Why Multiple Instructions Are Needed for MOD

OperationARM64 InstructionPurpose
Integer DivisionsdivCompute quotient
MultiplymulCompute (a / b) * b
SubtractsubGet remainder: a - (a / b) * b

Because ARM64 lacks a direct mod instruction, the compiler emulates it via:

MOD(a,b)=a(ab×b)\text{MOD}(a, b) = a - \left(\left\lfloor \frac{a}{b} \right\rfloor \times b\right)

Let me know if you’d like a version that works with negative numbers or Fortran’s MODULO() semantics (which differ subtly from MOD).

Explaining the Disassembly by Grok 4

Overview of the Disassembly

This disassembly is in ARM64 (AArch64) assembly language, representing the machine code generated from a simple Fortran program named mod_int. The Fortran source is provided inline for context:

program mod_int
    implicit none
    integer :: a, b, c
    a = 10
    b = 4
    c = MOD(a, b)
end program mod_int

This program declares three 32-bit integers (a, b, c), assigns a = 10 and b = 4, and computes c = MOD(10, 4), which should yield 2 (the remainder of 10 divided by 4). The result is stored but not used further (e.g., no printing or return value), so the program has no visible side effects when run—it simply computes and exits.

The disassembly consists of two main functions:

  1. MAIN__ at address 0x814: This is the compiler-generated name for the Fortran program’s body (common in gfortran). It allocates stack space for the three integers, performs the assignments and modulo operation, and returns.

  2. main at address 0x850: The C-style entry point for the executable, which handles command-line arguments, initializes the GNU Fortran (gfortran) runtime library, calls MAIN__, and exits with status 0.

This code was likely compiled with gfortran (evident from calls to _gfortran_set_args and _gfortran_set_options). It’s unoptimized, as seen in redundant loads (e.g., reloading b after division). The program uses 32-bit operations (w registers) for integers, matching Fortran’s default INTEGER type.

A key focus is the MOD(a, b) operation, which requires multiple instructions because ARM64 lacks a direct signed modulo instruction. Instead, the compiler implements it using signed division (sdiv), multiplication (mul), and subtraction (sub). This is a standard software emulation of modulo: a % b = a - ((a / b) * b), where / is integer division. This approach ensures correct handling of signed integers (e.g., negative values, though not used here), but it’s less efficient than a hypothetical hardware modulo. ARM64 does have unsigned divide-with-remainder (udiv + msub for multiply-subtract), but for signed operations like Fortran’s MOD, the compiler opts for this sequence to handle signs properly (using sdiv for signed division). If optimization were enabled (e.g., -O2), the compiler might inline constants or reduce redundancies, but here it’s straightforward.

I’ll explain each instruction in detail, including opcode, semantics, context, and binary encoding. ARM64 conventions: Registers x0-x30 (64-bit), w0-w30 (lower 32 bits); stack grows downward; ABI uses x0-x7 for params, x0 for returns, x29 for frame pointer, x30 for link register.

Detailed Explanation of MAIN__ (Fortran Program Logic)

This function allocates 16 bytes on the stack (for three 4-byte integers, padded for 16-byte alignment), assigns values to a and b, computes c = MOD(a, b) via the multi-step sequence, and cleans up. The nop is likely for alignment.

Detailed Explanation of main (Program Entry Point)

This is the runtime wrapper for Fortran executables. It processes args, sets up gfortran, calls MAIN__, and exits. Similar to the previous disassembly, with minor address differences.

Overall, the program computes but discards the modulo result. Running it produces no output. The multi-op modulo is a compiler choice for correctness and portability on ARM64.