ARM Assembly #2 - Understanding Logical Shifts (LSL, LSR)
Computers frequently perform bit-level operations to manipulate data, and shift operations are one of the most fundamental among them. In this post, we’ll focus on Logical Shifts in ARM assembly.
Rather than going deep into abstract theory, we’ll walk through simple examples that show how logical shift works in practice. We’ll also look at how the Carry Flag (C flag) is affected by these operations.
You can find the example code on the GitHub repository.
What is a shift operand?
In ARM assembly, instructions like mov, add, and sub allow one of the operands to be shifted on the fly before being used.
This is called a shift operand.
Thanks to this feature, you can perform shift and assignment in a single instruction, making the code more concise.
Example:
mov r1, r0, lsl #2 @ Shift R0 left by 2 bits and store in R1
What is a logical shift?
A logical shift simply moves the bits to the left or right, filling empty bits with 0.
For example, using 4-bit data 0110:
Left shift: 0110 << 1 = 1100
Right shift: 0110 >> 1 = 0011
ARM provides two logical shift operations:
| Instruction | Description |
|---|---|
lsl |
Logical Shift Left |
lsr |
Logical Shift Right |
The amount of shift can be specified either by an immediate value or by a register.
lsl #n,lsr #n: Immediate shiftlsl rN,lsr rN: Register shift
Example Code: Logical Shift in Action
logical-shift.s
.text
.global _start
_start:
mov r0, #3
mov r1, r0, lsl r0
mov r2, r1, lsr #1
b .mov r0, #3stores 3 inR0mov r1, r0, lsl r0shiftsR0left by 3 bits and stores inR1mov r2, r1, lsr #1shiftsR1right by 1 bit and stores inR2
For an introduction to the
movinstruction, please refer to the previous post
Example flow:
r0 = 0x3 = 0b0...000011
# r1 = r0 << 3
r1 = 0x18 = 0b0..011000
# r2 = 0x18 >> 1
r2= 0xC = 0b0...001100
Debugging with GDB
Let’s confirm the behavior using QEMU and GDB.
1. Run QEMU
$ qemu-system-arm \
-machine versatilepb \
-nographic \
-S \
-s \
-kernel logical-shift.elf
2. Connect with GDB
$ gdb-multiarch logical-shift.elf
(gdb) target remote localhost:1234
3. Check instructions
(gdb) x/10i 0x10000
4. Check registers
(gdb) info registers
(gdb) i r r0 r1 r2
5. Step Through
(gdb) stepi
After each step, check register values again to confirm the effect.
When is the Carry Flag set?
Shift operations push out bits that no longer fit in the range. One of those bits may be captured in the Carry Flag (C) in the CPSR register.
By default, instructions like mov do not update the carry flag.
To update it, use the s suffix: movs, adds, subs, etc.
Example Code: Checking the Carry Flag
logical-right-shift-carry.s
.text
.global _start
_start:
mov r0, 0x1
mov r1, r0, lsr #1
movs r2, r0, lsr #1
b .movdoes not affect the Carry flagmovsupdates the CPSR flags including Carry
To check the carry flag in GDB:
(gdb) target remote localhost:1234
(gdb) info registers cpsr
(gdb) p ($cpsr >> 29) & 1
This will show whether the Carry flag (bit 29) is set.
Limitations and Use Cases
Logical shift always fills in 0s. This works well for unsigned integers,
but can cause issues with signed values where the highest bit represents the sign.
Example:
mov r0, #-4
mov r1, r0, lsr #1
Shifting a signed number right with lsr can destroy the sign bit.
In these cases, use ASR (Arithmetic Shift Right) instead.
Despite this limitation, logical shift is extremely useful in:
- Array indexing
- Address calculations
- Bitmasking
Conclusion
In this post, we covered:
- How to use
lslandlsrin ARM assembly - What a shift operand is
- How to check the Carry flag using
movs - Why logical shifts are suitable for unsigned integers
In the next post, we’ll explore other shift types such as asr and ror.