Firstly, let's quickly recap what main data access instructions we have on both architectures:
- LDR - Loads a word from memory, and writes it to a register.
- LDRH - Loads a halfword from memory, zero-extends it to form a 32-bit word, and writes it to a register.
- LDRB - Loads a byte from memory, zero-extends it to form a 32-bit word, and writes it to a register.
- STR - Stores a word from a register to memory.
- STRH - Stores a halfword from a register to memory.
- STRB - Stores a byte from a register to memory.
Other variants of those instructions exist, but for purpose of this experiment let's stick to those basic ones listed above.
Note, there is a difference between ARMv6-M and ARMv7-M related to the alignment support.
ARMv6-M:
Note, there is a difference between ARMv6-M and ARMv7-M related to the alignment support.
ARMv6-M:
"ARMv6-M always generates a fault when an unaligned access occurs."ARMv7-M:
"The system architecture can choose one of two policies for alignment checking in ARMv7-M:
• Support the unaligned access
• Generate a fault when an unaligned access occurs.
The policy varies with the type of access. An implementation can be configured to force alignment faults for all unaligned accesses."
OK, so on ARMv6-M things are pretty simple:
Let's see a "normal" structure without any bit fields specified. Consider the following example:
Compile it (for now without optimizations):
And inspect:
You can do the same for ARMv7-M (by passing -march=armv7-m flag) to see minor differences between generated asm but it's not important for purpose of this consideration.
What we need to notice are two things*:
- Access using LDR/STR must be word aligned.
- Access using LDRH/STRH must be halfword aligned
- Byte access can be achieved using LDRB and STRB instructions.
Let's see a "normal" structure without any bit fields specified. Consider the following example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | struct { unsigned int a; unsigned int b; unsigned int c; unsigned int d; unsigned int e; } data = {6, 3, 1, 6, 57672}; int _start() { volatile unsigned int a = data.a; volatile unsigned int b = data.b; volatile unsigned int c = data.c; volatile unsigned int d = data.d; volatile unsigned int e = data.e; return 0; } |
Compile it (for now without optimizations):
arm-none-eabi-gcc -nostdlib -mthumb -O0 -march=armv6-m -nostdlib main.c -o test
And inspect:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | mk@mk-VirtualBox:~/test/bitfields$ arm-none-eabi-objdump -D test test: file format elf32-littlearm Disassembly of section .text: 00008000 <_start>: 8000: b580 push {r7, lr} 8002: b086 sub sp, #24 8004: af00 add r7, sp, #0 8006: 4b0a ldr r3, [pc, #40] ; (8030 <_start+0x30>) 8008: 681b ldr r3, [r3, #0] 800a: 617b str r3, [r7, #20] 800c: 4b08 ldr r3, [pc, #32] ; (8030 <_start+0x30>) 800e: 685b ldr r3, [r3, #4] 8010: 613b str r3, [r7, #16] 8012: 4b07 ldr r3, [pc, #28] ; (8030 <_start+0x30>) 8014: 689b ldr r3, [r3, #8] 8016: 60fb str r3, [r7, #12] 8018: 4b05 ldr r3, [pc, #20] ; (8030 <_start+0x30>) 801a: 68db ldr r3, [r3, #12] 801c: 60bb str r3, [r7, #8] 801e: 4b04 ldr r3, [pc, #16] ; (8030 <_start+0x30>) 8020: 691b ldr r3, [r3, #16] 8022: 607b str r3, [r7, #4] 8024: 2300 movs r3, #0 8026: 1c18 adds r0, r3, #0 8028: 46bd mov sp, r7 802a: b006 add sp, #24 802c: bd80 pop {r7, pc} 802e: 46c0 nop ; (mov r8, r8) 8030: 00010034 andeq r0, r1, r4, lsr r0 Disassembly of section .data: 00010034 <__data_start>: 10034: 00000006 10038: 00000003 1003c: 00000001 10040: 00000006 10044: 0000e148 |
You can do the same for ARMv7-M (by passing -march=armv7-m flag) to see minor differences between generated asm but it's not important for purpose of this consideration.
What we need to notice are two things*:
- The "data" structure occupies 20 bytes (5 words) in the memory (lines 38-42).
- Accessing fields (for example lines 13 or 16) is done using ldr instruction.
* Note, I'm not discussing padding between fields in a structure (not happening here because all fields are 32 bits anyway).
So far so good. All fields in the structure are integers which are 4 bytes each on both architectures. The "data" variable is a global, so it starts on a word aligned address. The whole word can be read using ldr instruction.
Now, suppose the structure represents a 32 bit register and its fields "a", "b", "c", "d" and "e" are respectively 4, 4, 1, 7 and 16 bits long:
To implement such structure we can use bit fields:So far so good. All fields in the structure are integers which are 4 bytes each on both architectures. The "data" variable is a global, so it starts on a word aligned address. The whole word can be read using ldr instruction.
Now, suppose the structure represents a 32 bit register and its fields "a", "b", "c", "d" and "e" are respectively 4, 4, 1, 7 and 16 bits long:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | struct { unsigned int a : 4; unsigned int b : 4; unsigned int c : 1; unsigned int d : 7; unsigned int e : 16; } data = {6, 3, 1, 6, 57672}; int _start() { volatile unsigned int a = data.a; volatile unsigned int b = data.b; volatile unsigned int c = data.c; volatile unsigned int d = data.d; volatile unsigned int e = data.e; return 0; } |
Without optimizations, GCC will now produce the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | mk@mk-VirtualBox:~/test/bitfields$ arm-none-eabi-objdump -D test test: file format elf32-littlearm Disassembly of section .text: 00008000 <_start>: 8000: b580 push {r7, lr} 8002: b086 sub sp, #24 8004: af00 add r7, sp, #0 8006: 4b10 ldr r3, [pc, #64] ; (8048 <_start+0x48>) 8008: 781b ldrb r3, [r3, #0] 800a: 071b lsls r3, r3, #28 800c: 0f1b lsrs r3, r3, #28 800e: b2db uxtb r3, r3 8010: 617b str r3, [r7, #20] 8012: 4b0d ldr r3, [pc, #52] ; (8048 <_start+0x48>) 8014: 781b ldrb r3, [r3, #0] 8016: 061b lsls r3, r3, #24 8018: 0f1b lsrs r3, r3, #28 801a: b2db uxtb r3, r3 801c: 613b str r3, [r7, #16] 801e: 4b0a ldr r3, [pc, #40] ; (8048 <_start+0x48>) 8020: 785b ldrb r3, [r3, #1] 8022: 07db lsls r3, r3, #31 8024: 0fdb lsrs r3, r3, #31 8026: b2db uxtb r3, r3 8028: 60fb str r3, [r7, #12] 802a: 4b07 ldr r3, [pc, #28] ; (8048 <_start+0x48>) 802c: 785b ldrb r3, [r3, #1] 802e: 061b lsls r3, r3, #24 8030: 0e5b lsrs r3, r3, #25 8032: b2db uxtb r3, r3 8034: 60bb str r3, [r7, #8] 8036: 4b04 ldr r3, [pc, #16] ; (8048 <_start+0x48>) 8038: 885b ldrh r3, [r3, #2] 803a: 607b str r3, [r7, #4] 803c: 2300 movs r3, #0 803e: 1c18 adds r0, r3, #0 8040: 46bd mov sp, r7 8042: b006 add sp, #24 8044: bd80 pop {r7, pc} 8046: 46c0 nop ; (mov r8, r8) 8048: 0001004c andeq r0, r1, ip, asr #32 Disassembly of section .data: 0001004c <__data_start>: 1004c: e1480d36 |
So, what's happening? Observations:
- To get "bit parts" processor will read a smallest possible chunk of data (see for instance line 13), then it will shift it left (line 14) and right (line 15) to get rid of unwanted bits.
- Generated code uses also uxtb instruction which is the "Unsigned Extend Byte" (extracts an 8-bit value from a register, zero extends it to 32 bits, and writes the result to the destination register).
- If it's possible it will use instructions that can read more than one byte (see line 37).
- Because the sum of our bit fields doesn't exceed the word size (4 + 4 + 1 + 7 + 16 <= 32) we use only 4 bytes of data (line 50).
According to the last observation: if we add at least one more field, we'll need a whole new word to store it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | struct { unsigned int a : 4; unsigned int b : 4; unsigned int c : 1; unsigned int d : 7; unsigned int e : 16; unsigned int f : 1; } data = {6, 3, 1, 6, 57672, 1}; int _start() { volatile unsigned int a = data.a; volatile unsigned int b = data.b; volatile unsigned int c = data.c; volatile unsigned int d = data.d; volatile unsigned int e = data.e; volatile unsigned int f = data.f; return 0; } |
With additional one-bit field "f" the new word is allocated (line 30):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | Disassembly of section .text: 00008000 <_start>: 8000: b580 push {r7, lr} 8002: b086 sub sp, #24 8004: af00 add r7, sp, #0 8006: 4b13 ldr r3, [pc, #76] ; (8054 <_start+0x54>) 8008: 781b ldrb r3, [r3, #0] 800a: 071b lsls r3, r3, #28 800c: 0f1b lsrs r3, r3, #28 (..) 803c: 4b05 ldr r3, [pc, #20] ; (8054 <_start+0x54>) 803e: 791b ldrb r3, [r3, #4] 8040: 07db lsls r3, r3, #31 8042: 0fdb lsrs r3, r3, #31 8044: b2db uxtb r3, r3 8046: 603b str r3, [r7, #0] 8048: 2300 movs r3, #0 804a: 1c18 adds r0, r3, #0 804c: 46bd mov sp, r7 804e: b006 add sp, #24 8050: bd80 pop {r7, pc} 8052: 46c0 nop ; (mov r8, r8) 8054: 00010058 andeq r0, r1, r8, asr r0 Disassembly of section .data: 00010058 <__data_start>: 10058: e1480d36 1005c: 00000001 |
OK, so this is how it works. Just for the reference, let's have a look into generated assembler with enabled optimizations :
arm-none-eabi-gcc -nostdlib -mthumb -Os -march=armv6-m -nostdlib main.c -o test |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | mk@mk-VirtualBox:~/test/bitfields$ arm-none-eabi-objdump -D test test: file format elf32-littlearm Disassembly of section .text: 00008000 <_start>: 8000: 4b0c ldr r3, [pc, #48] ; (8034 <_start+0x34>) 8002: b086 sub sp, #24 8004: 781a ldrb r2, [r3, #0] 8006: 2000 movs r0, #0 8008: 0711 lsls r1, r2, #28 800a: 0f09 lsrs r1, r1, #28 800c: 0912 lsrs r2, r2, #4 800e: 9100 str r1, [sp, #0] 8010: 9201 str r2, [sp, #4] 8012: 785a ldrb r2, [r3, #1] 8014: 07d1 lsls r1, r2, #31 8016: 0fc9 lsrs r1, r1, #31 8018: b2c9 uxtb r1, r1 801a: 0852 lsrs r2, r2, #1 801c: 9102 str r1, [sp, #8] 801e: 9203 str r2, [sp, #12] 8020: 885a ldrh r2, [r3, #2] 8022: 791b ldrb r3, [r3, #4] 8024: 9204 str r2, [sp, #16] 8026: 07db lsls r3, r3, #31 8028: 0fdb lsrs r3, r3, #31 802a: b2db uxtb r3, r3 802c: 9305 str r3, [sp, #20] 802e: b006 add sp, #24 8030: 4770 bx lr 8032: 46c0 nop ; (mov r8, r8) 8034: 00010038 andeq r0, r1, r8, lsr r0 Disassembly of section .data: 00010038 <__data_start>: 10038: e1480d36 1003c: 00000001 |
Just out of curiosity, let's also compile for ARMv7-M:
arm-none-eabi-gcc -nostdlib -mthumb -Os -march=armv7-m -nostdlib main.c -o test |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | mk@mk-VirtualBox:~/test/bitfields$ arm-none-eabi-objdump -D test test: file format elf32-littlearm Disassembly of section .text: 00008000 <_start>: 8000: 4b0b ldr r3, [pc, #44] ; (8030 <_start+0x30>) 8002: b086 sub sp, #24 8004: 781a ldrb r2, [r3, #0] 8006: 2000 movs r0, #0 8008: f002 010f and.w r1, r2, #15 800c: 0912 lsrs r2, r2, #4 800e: 9100 str r1, [sp, #0] 8010: 9201 str r2, [sp, #4] 8012: 785a ldrb r2, [r3, #1] 8014: f002 0101 and.w r1, r2, #1 8018: 0852 lsrs r2, r2, #1 801a: 9102 str r1, [sp, #8] 801c: 9203 str r2, [sp, #12] 801e: 885a ldrh r2, [r3, #2] 8020: 791b ldrb r3, [r3, #4] 8022: 9204 str r2, [sp, #16] 8024: f003 0301 and.w r3, r3, #1 8028: 9305 str r3, [sp, #20] 802a: b006 add sp, #24 802c: 4770 bx lr 802e: bf00 nop 8030: 00010034 andeq r0, r1, r4, lsr r0 Disassembly of section .data: 00010034 <__data_start>: 10034: e1480d36 10038: 00000001 |
No major differences between them. In both cases we see that after optimizations there will be less actual data read instructions (see bold lines), but, of course, there still will be the "bit shuffle" using shift operations.
Now, our example "data" variable was aligned by compiler. But in real life, the variable that stores bit fields can be wrongly cast or just moved to the unaligned address by mistake. Consider following dummy code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | struct data_t { unsigned int a : 4; unsigned int b : 4; unsigned int c : 1; unsigned int d : 7; unsigned int e : 16; unsigned int f : 1; } data = {6, 3, 1, 6, 57672, 1}; int _start() { volatile struct data_t *some_mem = (struct data_t *)0x10031; volatile unsigned int s = some_mem->a; return 0; } |
We deliberately pointed to the unaligned address. Although the field is only 4 bit long, compiler will not use ldrb instruction, because it assumes the beginning of the structure is aligned. Instead, it will use ldr instruction causing hard fault exception due to unaligned access (line 11):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | mk@mk-VirtualBox:~/test/bitfields$ arm-none-eabi-objdump -D test test: file format elf32-littlearm Disassembly of section .text: 00008000 <_start>: 8000: 4b04 ldr r3, [pc, #16] ; (8014 <_start+0x14>) 8002: b082 sub sp, #8 8004: 681b ldr r3, [r3, #0] 8006: 2000 movs r0, #0 8008: 071b lsls r3, r3, #28 800a: 0f1b lsrs r3, r3, #28 800c: 9301 str r3, [sp, #4] 800e: b002 add sp, #8 8010: 4770 bx lr 8012: 46c0 nop ; (mov r8, r8) 8014: 00010031 andeq r0, r1, r1, lsr r0 Disassembly of section .data: 00010018 <__data_start>: 10018: e1480d36 1001c: 00000001 |
Conclusions? It's good to be aware of data access alignment issues. I've started looking into this problem after investigation of a bug that originated from wrong casting of a structure that was using bit fields (on Cortex M0). Although the specific bit field may look like accessible by byte-aligned instruction, it's not always the case. Sometimes nothing wrong will happen because the accessed field will be luckily aligned by accident. What's more likely however: sooner or later you'll get a hard fault exception because of unaligned access. What's also worth noting is that on ARMv7-M (as opposed to ARMv6-M) the unaligned access can be hidden for the programmer and cause more bus accesses but no hard faults.