Psellos
Life So Short, the Craft So Long to Learn

Convert ARM Assembly Code for Apple’s iOS Assembler (Update 1)

July 19, 2012

I’m making reasonable progress in getting OCaml 4.00.0 working on iOS. Just now I was able to build the entire system in a hybrid form, where the compiler is built for the host (OS X i386) and the standard library is built for the target (iOS ARM). This is already pretty close to what you want for a cross compiler. I’m hoping to be able to test some generated code on an iOS device soon. At the same time, OCaml 4.00.0 has reached the release candidate stage, so things are really starting to cook.

While building the libraries for ARM, I learned more about the incompatibilities between the GNU ARM assembler (for the supported Linux targets of OCaml) and Apple’s ARM assembler for iOS. This led me to revamp my Python script that converts ARM assembly code from the current GNU format to the required Apple format. I also decided I should make the conversion upward compatible; in other words, the converted assembly file should continue to work in its originally supported Linux environments as well as in iOS.

Note: I wrote a new, improved version of this script, described in Convert Linux ARM Assembly Code for iOS (Update 3).

The advantage of using a script is that it keeps the changes consistent, and it will still be useful if arm.S is rewritten in the future. The script, named arm-as-to-ios, now does the following:

  • Declare the architecture for iOS. The possibilities are armv6 and armv7. Currently I’m concentrating on getting armv7 to work, but the generated file also assembles successfully for armv6.

  • Specify that armv7 code should use the more space-efficient Thumb encoding in iOS.

  • Generate declarations for functions, similar to the .type pseudo-op for the Linux targets. It appears that Apple requires .thumb_func declarations only for Thumb functions. Other symbols are apparently assumed to be ARM functions. To make this work upward-compatibly, I define an assembly macro named .funtype that is expanded properly in the different environments.

  • Make sure that assembler-local symbols have the right format. For GNU assemblers, they look like “.Lxxx”. For the Apple assembler, they look like “Lxxx”. To support upward compatibly, this is handled by a cpp macro named Loc() that generates the proper symbol form for each environment.

  • Replace uses of =value notation by explicit loads from memory. The usual ARM assemblers interpret ldr rM, =value to mean that the value should be loaded into register M immediately (using mov) if possible, and loaded from memory (using ldr with PC-relative addressing) otherwise. The Apple assembler seems not to support this. arm-as-to-ios replaces uses of =value with explicit memory loads, emitting the pool of values into the .text segment at the end of the file.

  • Remove uses of two pseudo-ops, .type and .size, when assembling for iOS. They aren’t supported by Apple’s assembler. This is done by defining null macros for them.

  • Define a macro cbz when using ARM encodings for iOS. The cbz instruction is Thumb-only. The definition replaces it with a pair of ARM instructions.

Another advantage of using a script is that it might be useful to other people who need to port ARM assembly code to iOS. Granted, there probably aren’t a lot of people doing this. But if you are, maybe the script will provide a useful starting point.

You can download the script here:

The full text of the script is also included at the end of this post.

As I expected, I’ve already updated the script based on what I’ve learned while doing the OCaml 4-on-iOS project. As I make changes, I’ll keep the linked script up to date. If there are more large changes I’ll make another post about them.

If you want to try out arm-as-to-ios, copy and paste the lines from the end of this post into a file named arm-as-to-ios, or download it from the above link. Mark it as a script with chmod:

$ chmod +x arm-as-to-ios

To use the script, specify the name of an ARM assembly file. If no files are given, the script processes its standard input.

The following small example demonstrates the translations that arm-as-to-ios performs. Here is a small file of Linux (non-Apple) ARM assembly language:

        .syntax unified
        .text
        .align  2
        .globl  example
        .type example, %function
example:
        sub     r10, r10, 8
        cmp     r10, r11
        bcc     1f
        bx      lr
1:
        ldr     r7, =last_return_address
        str     lr, [r7]
        bl      .Lcall_gc
        ldr     lr, [r7]
        b       example

.Lcall_gc:
        ldr     r12, =bottom_of_stack
        str     sp, [r12]
        bl      garbage_collection
        bx      lr

If you run arm-as-to-ios on this file, you get the following output that works for all assemblers (Linux and Apple):

        .syntax unified

/* Apple compatibility macros */
#if defined(SYS_macosx)
#define Loc(s) L##s
#if defined(MODEL_armv6)
        .machine  armv6
        .macro  .funtype
        .endm
        .macro  cbz
        cmp     $0, #0
        beq     $1
        .endm
#else
        .machine  armv7
        .thumb
        .macro  .funtype
        .thumb_func $0
        .endm
#endif
        .macro  .type
        .endm
        .macro  .size
        .endm
#else
#define Loc(s) .L##s
        .macro  .funtype symbol
        .type  \symbol, %function
        .endm
#endif
        .text
        .align  2
        .globl  example
        .funtype  example
example:
        sub     r10, r10, 8
        cmp     r10, r11
        bcc     1f
        bx      lr
1:
        ldr     r7, Loc(Plast_return_address)
        str     lr, [r7]
        bl      Loc(call_gc)
        ldr     lr, [r7]
        b       example

Loc(call_gc):
        ldr     r12, Loc(Pbottom_of_stack)
        str     sp, [r12]
        bl      garbage_collection
        bx      lr

/* Pool of addresses loaded into registers */

        .text
        .align 2
Loc(Plast_return_address):
        .long last_return_address
Loc(Pbottom_of_stack):
        .long bottom_of_stack

The output consists of a fixed prefix followed by the translation of the input file, followed by the pool of values to be loaded into registers.

The following shows a successful assembly of arm.S from OCaml 4.00.0:

$ PLT=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform
$ PLTBIN=$PLT/Developer/usr/bin
$ arm-as-to-ios asmrun/arm.S > armios.S
$ $PLTBIN/gcc -c -arch armv7 -DSYS_macosx -DMODEL_armv7 -o armios.o armios.S
$ file armios.o
armios.o: Mach-O object arm
$ otool -tv armios.o | head
armios.o:
(__TEXT,__text) section
caml_call_gc:
00000000        f8dfc1e0        ldr.w   ip, [pc, #480]  @ 0x1e4
00000004        f8cce000        str.w   lr, [ip]
00000008        f8dfc1dc        ldr.w   ip, [pc, #476]  @ 0x1e8
0000000c        f8ccd000        str.w   sp, [ip]
00000010        ed2d0b10        vstmdb  sp!, {d0-d7}
00000014        e92d50ff        stmdb   sp!, {r0, r1, r2, r3, r4, r5, r6, r7, ip, lr}
00000018        f8dfc1d0        ldr.w   ip, [pc, #464]  @ 0x1ec

If you have any corrections, improvements, or other comments, leave them below or email me at jeffsco@psellos.com. I’d be very pleased to hear if the script has been helpful to anyone.

Posted by: Jeffrey

Appendix

Here is the current text of the script:

#!/usr/bin/env python
#
# arm-as-to-ios     Modify ARM assembly code for the iOS assembler
#
# Copyright (c) 2012 Psellos   http://psellos.com/
# Licensed under the MIT License:
#     http://www.opensource.org/licenses/mit-license.php
#
# Resources for running OCaml on iOS: http://psellos.com/ocaml/
#
import sys
import re

VERSION = '1.1.0'


def add_prefix(instrs):
    # Add compatibility macros for all systems, plus hardware
    # definitions and compatibility macros for iOS.
    #
    # All systems:
    #
    # Loc()     cpp macro for making local symbols (.Lxxx vs Lxxx)
    # .funtype  Expands to .thumb_func for iOS armv7 (null for armv6)
    #           Expands to .type %function for others
    #
    # iOS:
    #
    # .machine  armv6/armv7
    # .thumb    (for armv7)
    # cbz       Expands to cmp/beq for armv6 (Thumb-only instr)
    # .type     Not supported by Apple assembler
    # .size     Not supported by Apple assembler
    #
    defre = '#[ \t]*if.*def.*SYS'  # Add new defs near first existing ones
    skipre = '$|\.syntax[ \t]'     # Skip comments lines (and .syntax)

    for i in range(len(instrs)):
        if re.match(defre, instrs[i][1]):
            break
    else:
        i = 0
    for i in range(i, len(instrs)):
        if not re.match(skipre, instrs[i][1]):
            break
    instrs[i:0] = [
        ('', '', '\n'),
        ('/* Apple compatibility macros */', '', '\n'),
        ('', '#if defined(SYS_macosx)', '\n'),
        ('', '#define Loc(s) L##s', '\n'),
        ('', '#if defined(MODEL_armv6)', '\n'),
        ('        ', '.machine  armv6', '\n'),
        ('        ', '.macro  .funtype', '\n'),
        ('        ', '.endm', '\n'),
        ('        ', '.macro  cbz', '\n'),
        ('        ', 'cmp     $0, #0', '\n'),
        ('        ', 'beq     $1', '\n'),
        ('        ', '.endm', '\n'),
        ('', '#else', '\n'),
        ('        ', '.machine  armv7', '\n'),
        ('        ', '.thumb', '\n'),
        ('        ', '.macro  .funtype', '\n'),
        ('        ', '.thumb_func $0', '\n'),
        ('        ', '.endm', '\n'),
        ('', '#endif', '\n'),
        ('        ', '.macro  .type', '\n'),
        ('        ', '.endm', '\n'),
        ('        ', '.macro  .size', '\n'),
        ('        ', '.endm', '\n'),
        ('', '#else', '\n'),
        ('', '#define Loc(s) .L##s', '\n'),
        ('        ', '.macro  .funtype symbol', '\n'),
        ('        ', '.type  \\symbol, %function', '\n'),
        ('        ', '.endm', '\n'),
        ('', '#endif', '\n')
    ]
    return instrs


# Prefix for pooled symbols.  It's in the space of local symbols for the
# input file.  Later rules will modify this to the Loc() form.
#
g_prefix = '.LP'

# Regular expression for modified ldr lines
#
g_ldre = '(ldr[ \t][^,]*,[ \t]*)=(([^ \t\n@,/]|/(?!\*))*)(.*)'


def explicit_address_loads(instrs):
    # The Gnu assembler allows the following:
    #
    #     ldr rM, =symbol
    #
    # which loads rM with [mov] (immediately) if possible, or creates an
    # entry in memory for the symbol value and loads it PC-relatively
    # with [ldr].
    #
    # The Apple assembler doesn't seem to support this notation.  If the
    # value is a suitable constant, it emits a valid [mov].  Otherwise
    # it seems to emit an invalid [ldr] that always generates an error.
    # (At least I have not been able to make it work).  So, change uses
    # of =symbol to explicit PC-relative loads.
    #
    # This requires a pool containing the addresses to be loaded.  For
    # now, we just keep track of it ourselves and emit it into the text
    # segment at the end of the file.
    #
    syms = {}
    result = []

    def repl1((syms, result), (a, b, c)):
        global g_prefix
        global g_ldre
        mo = re.match(g_ldre, b, re.DOTALL)
        if mo:
            if mo.group(2) not in syms:
                syms[mo.group(2)] = len(syms)
            psym = mo.group(2)
            if psym[0:2] == '.L':
                psym = psym[2:]
            newb = mo.group(1) + g_prefix + psym + mo.group(4)
            result.append((a, newb, c))
        else:
            result.append((a, b, c))
        return (syms, result)

    def pool1(result, s):
        global g_prefix
        psym = s
        if psym[0:2] == '.L':
            psym = psym[2:]
        result.append(('', g_prefix + psym + ':', '\n'))
        result.append(('        ', '.long ' + s, '\n'))
        return result

    reduce(repl1, instrs, (syms, result))
    if len(syms) > 0:
        result.append(('', '', '\n'))
        result.append(('/* Pool of addresses loaded into registers */',
                        '', '\n'))
        result.append(('', '', '\n'))
        result.append(('        ', '.text', '\n'))
        result.append(('        ', '.align 2', '\n'))
        reduce(pool1, sorted(syms, key=syms.get), result)
    return result


def local_symbols(instrs):
    # The form of a local symbol differs between Gnu assemblers and the
    # Apple assember:
    #
    # Gnu:   .Lxxx
    # Apple: Lxxx
    #
    # Change occurrences of local symbols to use the Loc() cpp macro
    # defined in our prefix.
    #
    lsyms = set()
    result = []

    def find1 (lsyms, (a, b, c)):
        mo = re.match('(\.L[^ \t:]*)[ \t]*:', b)
        if mo:
            lsyms.add(mo.group(1))
        return lsyms

    def repl1((lsyms, result), (a, b, c)):
        matches = list(re.finditer('\.L[^ \t@:,+*/-]+', b))
        if matches != []:
            matches.reverse()
            newb = b
            for mo in matches:
                if mo.group() in lsyms:
                    newb = newb[0:mo.start()] + \
                            'Loc(' + mo.group()[2:] + ')' + \
                            newb[mo.end():]
            result.append((a, newb, c))
        else:
            result.append((a, b, c))
        return (lsyms, result)

    reduce(find1, instrs, lsyms)
    reduce(repl1, instrs, (lsyms, result))
    return result


def funtypes(instrs):
    # Gnu assemblers accept declarations like this:
    #
    #     .type  symbol, %function
    #
    # For Thumb functions, the Apple assembler wants to see:
    #
    #     .thumb_func symbol
    #
    # Handle this by converting declarations to this:
    #
    #     .funtype symbol
    #
    # Our prefix defines an appropriate .funtype macro for each
    # environment.
    #
    result = []

    def repl1(result, (a, b, c)):
        mo = re.match('.type[ \t]+([^ \t,]*),[ \t]*%function', b)
        if mo:
            result.append((a, '.funtype  ' + mo.group(1), c))
        else:
            result.append((a, b, c))
        return result

    reduce(repl1, instrs, result)
    return result


def read_input():
    # Concatenate all the input files into a string.
    #
    def fnl(s):
        if s == '' or s[-1] == '\n':
            return s
        else:
            return s + '\n'

    if len(sys.argv) < 2:
        return fnl(sys.stdin.read())
    else:
        input = ""
        for f in sys.argv[1:]:
            try:
                fd = open(f)
                input = input + fnl(fd.read())
                fd.close()
            except:
                sys.stderr.write('arm-as-to-ios: cannot open ' + f + '\n')
        return input


def parse_instrs(s):
    # Parse the string into assembly instructions, also noting C
    # preprocessor lines.  Each instruction is represented as a triple:
    # (space/comments, instruction, end).  The end is either ';' or
    # '\n'.  Instructions might have embedded comments, but they
    # probably won't get fixed up if they do.  (I've never seen it in
    # real code.)
    #
    def goodmo(mo):
        if mo == None:
            # Should never happen
            sys.stderr.write('arm-as-to-ios: internal parsing error\n')
            sys.exit(1)

    cpp_re = '([ \t]*)(#([^\n]*\\\\\n)*[^\n]*[^\\\\\n])\n'
    comment_re = '[ \t]*#[^\n]*'
    instr_re = (
        '(([ \t]|/\*.*?\*/|@[^\n]*)*)'  # Spaces & comments
        '(([ \t]|/\*.*?\*/|[^;\n])*)'   # "Instruction"
        '([;\n])'                       # End
    )
    instrs = []
    while s != '':
        if re.match('[ \t]*#[ \t]*(if|ifdef|elif|else|endif|define)', s):
            mo = re.match(cpp_re, s)
            goodmo(mo)
            instrs.append((mo.group(1), mo.group(2), '\n'))
        elif re.match('[ \t]*#', s):
            mo = re.match(comment_re, s)
            goodmo(mo)
            instrs.append((mo.group(0), '', '\n'))
        else:
            mo = re.match(instr_re, s, re.DOTALL)
            goodmo(mo)
            instrs.append((mo.group(1), mo.group(3), mo.group(5)))
        s = s[len(mo.group(0)):]
    return instrs


def main():
    instrs = parse_instrs(read_input())
    instrs = explicit_address_loads(instrs)
    instrs = funtypes(instrs)
    instrs = local_symbols(instrs)
    instrs = add_prefix(instrs)
    for (a, b, c) in instrs:
        sys.stdout.write(a + b + c)
#        sys.stdout.write('{' + a + '}' + '{' + b + '}' + c)


main()

Comments

blog comments powered by Disqus