mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3
2695 words
13 minutes
CH1.28 Manipulating specific bit(s) (part2)
2026-05-23

1.28.4 Setting and clearing specific bits: FPU example


Here is the layout of the bits present in the float type in IEEE 754 format:

IEEE 754 float bit layout

Since I don't like passing over something without explaining and interpreting it, let me break this down quickly:

* S (Sign) — the highest bit (Most Significant Bit - MSB). If it is 0 the number is positive, if 1 the number is negative.

* Exponent — 8 bits that determine the power of 2 for the number, but in a biased representation (add 127).

* Mantissa — 23 bits that define the digits after the decimal point.

So if you want to change the sign of a number from positive to negative or vice versa, all you need to do is flip bit 31 from 0 to 1 or from 1 to 0. The rest of the number (the exponent and mantissa) stays exactly as it is.

Getting back to our topic — the sign of the number lives in the highest bit (MSB). Is it possible to change the sign of a floating point number without any FPU instructions?

#include <stdio.h>
float my_abs (float i)
{
// clear bit 31 (sign bit) — force the number to be positive
unsigned int tmp = (*(unsigned int*)&i) & 0x7FFFFFFF; // AND with mask: all 1s except bit 31
return *(float*)&tmp; // reinterpret the modified bits as float
};
float set_sign (float i)
{
// set bit 31 (sign bit) — force the number to be negative
unsigned int tmp = (*(unsigned int*)&i) | 0x80000000; // OR with mask: only bit 31 is 1
return *(float*)&tmp; // reinterpret the modified bits as float
};
float negate (float i)
{
// flip bit 31 (sign bit) — toggle the sign
unsigned int tmp = (*(unsigned int*)&i) ^ 0x80000000; // XOR with mask: flips only bit 31
return *(float*)&tmp; // reinterpret the modified bits as float
};
int main()
{
printf ("my_abs():\n");
printf ("%f\n", my_abs (123.456)); // should print 123.456
printf ("%f\n", my_abs (-456.123)); // should print 456.123
printf ("set_sign():\n");
printf ("%f\n", set_sign (123.456)); // should print -123.456
printf ("%f\n", set_sign (-456.123)); // should print -456.123
printf ("negate():\n");
printf ("%f\n", negate (123.456)); // should print -123.456
printf ("%f\n", negate (-456.123)); // should print 456.123
};

We need this trick in C/C++ to copy to and from a float value without any actual conversion. There are three functions: my_abs() clears the MSB (sets it to 0); set_sign() sets the MSB (forces it to 1); and negate() flips it. XOR can be used to flip a bit.


x86

The code is straightforward at this point.

Listing 1.286: Optimizing MSVC 2012

_tmp$ = 8
_i$ = 8
_my_abs PROC
and DWORD PTR _i$[esp-4], 2147483647 ; AND with 0x7FFFFFFF — clear bit 31 (sign bit)
fld DWORD PTR _tmp$[esp-4] ; load the modified float from stack into ST0 (FPU return register)
ret 0
_my_abs ENDP
_tmp$ = 8
_i$ = 8
_set_sign PROC
or DWORD PTR _i$[esp-4], -2147483648 ; OR with 0x80000000 — set bit 31 (sign bit)
fld DWORD PTR _tmp$[esp-4] ; load the modified float into ST0
ret 0
_set_sign ENDP
_tmp$ = 8
_i$ = 8
_negate PROC
xor DWORD PTR _i$[esp-4], -2147483648 ; XOR with 0x80000000 — flip bit 31 (sign bit)
fld DWORD PTR _tmp$[esp-4] ; load the modified float into ST0
ret 0
_negate ENDP

A quick explanation:

* DWORD PTR _i$[esp-4] — points to the first parameter on the stack (because in Win32 the function receives a float on the stack).

* and ... 0x7FFFFFFF — modifies the bit directly in memory.

* fld — loads the modified value from its new location into the FPU register ST0 in order to return it.

AND clears and OR sets the desired bit. XOR flips it. Finally, the modified value is loaded into ST0, because floating point values are returned in that register.

Now let's try MSVC 2012 optimizing for x64:

tmp$ = 8
i$ = 8
my_abs PROC
movss DWORD PTR [rsp+8], xmm0 ; spill float argument from XMM0 onto stack
mov eax, DWORD PTR i$[rsp] ; load the 32-bit representation into EAX
btr eax, 31 ; BTR: test bit 31 then reset (clear) it
mov DWORD PTR tmp$[rsp], eax ; store modified value back onto stack
movss xmm0, DWORD PTR tmp$[rsp] ; load modified float into XMM0 (return register in Win64)
ret 0
my_abs ENDP
set_sign PROC
movss DWORD PTR [rsp+8], xmm0 ; spill float argument from XMM0 onto stack
mov eax, DWORD PTR i$[rsp] ; load the 32-bit representation into EAX
bts eax, 31 ; BTS: test bit 31 then set it
mov DWORD PTR tmp$[rsp], eax ; store modified value back onto stack
movss xmm0, DWORD PTR tmp$[rsp] ; load modified float into XMM0
ret 0
set_sign ENDP
negate PROC
movss DWORD PTR [rsp+8], xmm0 ; spill float argument from XMM0 onto stack
mov eax, DWORD PTR i$[rsp] ; load the 32-bit representation into EAX
btc eax, 31 ; BTC: test bit 31 then complement (flip) it
mov DWORD PTR tmp$[rsp], eax ; store modified value back onto stack
movss xmm0, DWORD PTR tmp$[rsp] ; load modified float into XMM0
ret 0
negate ENDP

Here the first float parameter arrives in xmm0 and is temporarily copied to the local stack. Then three new instructions appear:

* BTR (Bit Test and Reset) — tests the bit value (puts it in the Carry Flag), then clears it (sets it to zero). A shorthand for AND with an inverted mask.

* BTS (Bit Test and Set) — tests then sets the bit.

* BTC (Bit Test and Complement) — tests then flips the bit.

Finally, the result is copied into XMM0, because floating point values are returned through XMM0 in the Win64 environment.


MIPS

my_abs:
; move from coprocessor 1 (FPU) to integer register:
mfc1 $v1, $f12 ; $v1 = float argument from FPU register $f12
li $v0, 0x7FFFFFFF ; $v0 = 0x7FFFFFFF (mask to clear sign bit)
and $v0, $v1 ; $v0 = $v1 & 0x7FFFFFFF — clear bit 31
mtc1 $v0, $f0 ; move result back to FPU register $f0 (return value)
jr $ra ; return
or $at, $zero ; branch delay slot (NOP)
set_sign:
; move from coprocessor 1 (FPU) to integer register:
mfc1 $v0, $f12 ; $v0 = float argument from FPU register $f12
lui $v1, 0x8000 ; $v1 = 0x80000000 (load upper immediate: 0x8000 << 16)
or $v0, $v1, $v0 ; $v0 = $v0 | 0x80000000 — set bit 31
mtc1 $v0, $f0 ; move result back to FPU register $f0
jr $ra ; return
or $at, $zero ; branch delay slot (NOP)
negate:
; move from coprocessor 1 (FPU) to integer register:
mfc1 $v0, $f12 ; $v0 = float argument from FPU register $f12
lui $v1, 0x8000 ; $v1 = 0x80000000
xor $v0, $v1, $v0 ; $v0 = $v0 ^ 0x80000000 — flip bit 31
mtc1 $v0, $f0 ; move result back to FPU register $f0
jr $ra ; return
or $at, $zero ; branch delay slot (NOP)

GCC 4.4.5 for MIPS does roughly the same thing:

Listing 1.288: Optimizing GCC 4.4.5 (IDA)

my_abs:
mfc1 $v1, $f12 ; Move From Coprocessor 1: transfer float from $f12 to integer $v1
li $v0, 0x7FFFFFFF ; load mask 0x7FFFFFFF into $v0
and $v0, $v1 ; $v0 = float_bits & 0x7FFFFFFF — clear sign bit
mtc1 $v0, $f0 ; Move To Coprocessor 1: transfer result to FPU return register $f0
jr $ra ; return
or $at, $zero ; branch delay slot (NOP)
set_sign:
mfc1 $v0, $f12 ; transfer float from $f12 to integer $v0
lui $v1, 0x8000 ; $v1 = 0x8000 << 16 = 0x80000000
or $v0, $v1, $v0 ; $v0 = float_bits | 0x80000000 — set sign bit
mtc1 $v0, $f0 ; transfer result to FPU return register $f0
jr $ra ; return
or $at, $zero ; branch delay slot (NOP)
negate:
mfc1 $v0, $f12 ; transfer float from $f12 to integer $v0
lui $v1, 0x8000 ; $v1 = 0x80000000
xor $v0, $v1, $v0 ; $v0 = float_bits ^ 0x80000000 — flip sign bit
mtc1 $v0, $f0 ; transfer result to FPU return register $f0
jr $ra ; return
or $at, $zero ; branch delay slot (NOP)

A quick explanation:

* mfc1 $v1, $f12 — Move From Coprocessor 1. Transfers from $f12 (first parameter) to $v1 (integer CPU register).

* The modification is done using and/or/xor with the appropriate mask.

* mtc1 $v0, $f0 — Move To Coprocessor 1. Returns the result in $f0 (FPU return register).

Loading 0x80000000 is done with lui $v1, 0x8000 (Load Upper Immediate): it places 0x8000 in the upper 16 bits and zeros the lower 16 bits, producing 0x80000000 because 0x8000 << 16 = 0x80000000. This saves us from needing an extra ori.


ARM

Optimizing Keil 6/2013 (ARM mode)

my_abs PROC
BIC r0, r0, #0x80000000 ; clear bit 31 — BIC = Bitwise bit Clear (AND with inverted mask)
BX lr ; return
ENDP
set_sign PROC
ORR r0, r0, #0x80000000 ; set bit 31 — ORR = logical OR
BX lr ; return
ENDP
negate PROC
EOR r0, r0, #0x80000000 ; flip bit 31 — EOR = Exclusive OR (ARM name for XOR)
BX lr ; return
ENDP

All good so far. ARM has the BIC instruction that clears specific bits explicitly. EOR is the ARM instruction name for the XOR operation ("Exclusive OR").

Optimizing Keil 6/2013 (Thumb mode)

my_abs PROC
LSLS r0, r0, #1 ; R0 = i << 1 (shifts out the sign bit MSB)
LSRS r0, r0, #1 ; R0 = (i << 1) >> 1 (shifts back, MSB is now 0)
BX lr ; return
ENDP
set_sign PROC
MOVS r1, #1 ; R1 = 1
LSLS r1, r1, #31 ; R1 = 1 << 31 = 0x80000000 (build the mask)
ORRS r0, r0, r1 ; R0 = R0 | 0x80000000 — set bit 31
BX lr ; return
ENDP
negate PROC
MOVS r1, #1 ; R1 = 1
LSLS r1, r1, #31 ; R1 = 1 << 31 = 0x80000000 (build the mask)
EORS r0, r0, r1 ; R0 = R0 ^ 0x80000000 — flip bit 31
BX lr ; return
ENDP

Thumb mode in ARM offers 16-bit instructions, and not much data can be encoded in them, so the MOVS/LSLS instruction pair is used here to build the constant 0x80000000. It works like this: 1 << 31 = 0x80000000.

The my_abs code looks strange — it is effectively doing (i << 1) >> 1. That expression seems meaningless. But when input << 1 executes, the MSB (the sign bit) gets dropped. When the result is then >> 1, all bits shift back to their positions, but the MSB is now zero, because all "new" bits that appear from shift operations are always zeros. So the LSLS/LSRS pair effectively clears the MSB.


Optimizing GCC 4.6.3 (Raspberry Pi, ARM mode)

Listing 1.291: Optimizing GCC 4.6.3 for Raspberry Pi (ARM mode)

my_abs:
FMRS R2, S0 ; copy float from FPU register S0 to integer register R2
BIC R3, R2, #0x80000000 ; clear bit 31 — R3 = R2 & ~0x80000000
FMSR S0, R3 ; copy result from R3 back to FPU register S0 (return value)
BX LR ; return
set_sign:
FMRS R2, S0 ; copy float from FPU register S0 to integer register R2
ORR R3, R2, #0x80000000 ; set bit 31 — R3 = R2 | 0x80000000
FMSR S0, R3 ; copy result back to FPU register S0
BX LR ; return
negate:
FMRS R2, S0 ; copy float from FPU register S0 to integer register R2
ADD R3, R2, #0x80000000 ; flip bit 31 using ADD — equivalent to XOR here (see explanation below)
FMSR S0, R3 ; copy result back to FPU register S0
BX LR ; return

We are running Raspberry Pi Linux in QEMU which emulates the ARM FPU, so S-registers are used here for floating point numbers instead of R-registers.

The FMRS instruction moves data between GPR (general purpose registers) and the FPU and vice versa.

my_abs() and set_sign() work as expected, but what about negate()? Why is there ADD instead of XOR?

Hard to believe, but the instruction ADD register, 0x80000000 works exactly like XOR register, 0x80000000. First, what is our goal? The goal is to flip the MSB. Forget XOR for a moment. From school math we may remember that adding values like 1000 to other numbers does not affect the last 3 digits at all. Example: 1234567 + 10000 = 1244567 (the last 4 digits are unaffected).

But here we are working in binary, and 0x80000000 is 0b10000000000000000000000000000000 — meaning only the highest bit is set.

Adding 0x80000000 to any value never affects the lower 31 bits — it only affects the MSB. Adding 1 to 0 gives 1. Adding 1 to 1 gives 0b10 in binary, but bit 32 (counting from zero) gets dropped because our registers are 32 bits wide, so the result becomes 0. That is why XOR can be replaced by ADD here. It is hard to say why GCC decided to do it this way, but it works correctly.

1.28.5 Counting bits set to 1

Counting

This is a simple example of a function that counts the number of bits whose value is 1 in the input value.

These operations are also called "population count".

#include <stdio.h>
#define IS_SET(flag, bit) ((flag) & (bit)) // macro: checks if a specific bit is set in flag using bitwise AND
int f(unsigned int a)
{
int i;
int rt = 0; // bit counter, starts at 0
for (i = 0; i < 32; i++) // iterate over all 32 bit positions
if (IS_SET(a, 1 << i)) // check if bit i is set in a
rt++; // if set, increment counter
return rt; // return total count of set bits
}
int main()
{
f(0x12345678); // test call
}

In this loop, the counter i counts from 0 to 31, so 1 << i produces values from 1 to 0x80000000. To describe this operation in natural language, we say "shift 1 left by n bits". In other words, 1 << i sequentially produces every possible bit position in a 32-bit number. The bit that is freed on the right is always zeroed (set to 0).

C/C++ expression Power of 2 Decimal Hexadecimal
1 << 02^011
1 << 12^122
1 << 22^244
1 << 32^388
1 << 42^4160x10
1 << 52^5320x20
1 << 62^6640x40
1 << 72^71280x80
1 << 82^82560x100
1 << 92^95120x200
1 << 102^1010240x400
1 << 112^1120480x800
1 << 122^1240960x1000
1 << 132^1381920x2000
1 << 142^14163840x4000
1 << 152^15327680x8000
1 << 162^16655360x10000
1 << 172^171310720x20000
1 << 182^182621440x40000
1 << 192^195242880x80000
1 << 202^2010485760x100000
1 << 212^2120971520x200000
1 << 222^2241943040x400000
1 << 232^2383886080x800000
1 << 242^24167772160x1000000
1 << 252^25335544320x2000000
1 << 262^26671088640x4000000
1 << 272^271342177280x8000000
1 << 282^282684354560x10000000
1 << 292^295368709120x20000000
1 << 302^3010737418240x40000000
1 << 312^3121474836480x80000000

These constants (bit masks) appear very frequently in code, and the reverse engineer must be able to recognize them quickly. Decimal numbers below 65536 and hexadecimal ones are very easy to memorize. Decimal numbers above 65536 are generally not worth memorizing. These constants are used very heavily to set flags for specific bits. Example: a portion of ssl_private.h from Apache 2.4.6 source code:

#define SSL_OPT_NONE (0) // no options set
#define SSL_OPT_RELSET (1<<0) // bit 0: relative set
#define SSL_OPT_STDENVVARS (1<<1) // bit 1: standard environment variables
#define SSL_OPT_EXPORTCERTDATA (1<<3) // bit 3: export certificate data
#define SSL_OPT_FAKEBASICAUTH (1<<4) // bit 4: fake basic auth
#define SSL_OPT_STRICTREQUIRE (1<<5) // bit 5: strict require
#define SSL_OPT_OPTRENEGOTIATE (1<<6) // bit 6: optional renegotiation
#define SSL_OPT_LEGACYDNFORMAT (1<<7) // bit 7: legacy DN format

Let's return to our example.

The IS_SET macro checks for the presence of a bit in a. The IS_SET macro is in fact an AND operation (logical AND) and returns 0 if the specified bit is not present, or returns the bit mask itself if the bit is present. The if statement in C/C++ fires if the expression inside it is non-zero (it can even be 123456), and that is why it always works correctly.


x86

MSVC

Listing 1.292: MSVC 2010

_rt$ = -8 ; size = 4 bytes
_i$ = -4 ; size = 4 bytes
_a$ = 8 ; size = 4 bytes
_f PROC
push ebp
mov ebp, esp
sub esp, 8 ; allocate 8 bytes on stack for rt and i
mov DWORD PTR _rt$[ebp], 0 ; rt = 0
mov DWORD PTR _i$[ebp], 0 ; i = 0
jmp SHORT $LN4@f ; jump to loop condition check
$LN3@f:
mov eax, DWORD PTR _i$[ebp] ; load i into EAX
add eax, 1 ; EAX = i + 1
mov DWORD PTR _i$[ebp], eax ; i++
$LN4@f:
cmp DWORD PTR _i$[ebp], 32 ; compare i with 32 (0x20)
jge SHORT $LN2@f ; if i >= 32, exit loop
mov edx, 1 ; EDX = 1 (base bit mask)
mov ecx, DWORD PTR _i$[ebp] ; ECX = i (shift amount)
shl edx, cl ; EDX = 1 << i (build the bit mask)
and edx, DWORD PTR _a$[ebp] ; EDX = mask & a (test if bit i is set)
je SHORT $LN1@f ; AND result was 0? skip increment
mov eax, DWORD PTR _rt$[ebp] ; load rt
add eax, 1 ; rt + 1
mov DWORD PTR _rt$[ebp], eax ; rt++
$LN1@f:
jmp SHORT $LN3@f ; loop back to increment
$LN2@f:
mov eax, DWORD PTR _rt$[ebp] ; return rt in EAX
mov esp, ebp
pop ebp
ret 0
_f ENDP

x32dbg

We will run the above example in x32dbg, using the value 0x12345678 (God willing I will explain it in detail so everything is clear and easy).

bits_1

Let's step into the first iteration when i = 0, and see how i is loaded into ECX:

The first instruction mov edx, 1 places the value 1 into EDX — this is the mask for bit zero.

Then the instruction mov ecx, dword ptr ss:[ebp-0x04] loads the value of i into ECX, which is 0 since this is the beginning of the loop.

Then the instruction shl edx, cl — the most important one (Shift Logical Left) — shifts the value of EDX (which is 1) left by the number of bits in CL (the low byte of ECX, containing i=0). Result: 1 << 0 = 1.

So EDX is still 1, and it is the mask that will check bit zero.

bits_2

After that it will execute je.

When i = 1, we see how i is loaded into ECX:

bits_3

EDX holds 1. And SHL will now execute.

bits_4

EDX now holds 1 << 1 (i.e. 2). This is the bit mask as we said before.

The AND sets ZF to 1, which means that the input value (0x12345678) ANDed with 2:

bits_5

So, there is no corresponding bit set in the input value.

The part of the code that increments the counter will not execute: the JZ instruction skips it.

Let's trace a bit more. Now i = 4. SHL will execute:

bits_6

EDX = 1 << 4 (i.e. 0x10 or 16):

bits_7

Another mask.

When the AND executes:

bits_8

ZF became 0 because this bit is present in the input value.

Indeed, 0x12345678 & 0x10 = 0x10. This bit gets counted: the jump does not take place, and the bit counter is incremented.

The function returns 13. That is the total count of bits set to 1 in 0x12345678.


GCC

Let's compile it in GCC 4.4.1:

Listing 1.293: GCC 4.4.1

public f
f proc near
rt = dword ptr -0Ch ; local variable: bit counter
i = dword ptr -8 ; local variable: loop index
arg_0 = dword ptr 8 ; function argument: input value a
push ebp
mov ebp, esp
push ebx ; save EBX (callee-saved register)
sub esp, 10h ; allocate 16 bytes on stack
mov [ebp+rt], 0 ; rt = 0
mov [ebp+i], 0 ; i = 0
jmp short loc_80483EF ; jump to loop condition check
loc_80483D0:
mov eax, [ebp+i] ; load i into EAX (shift amount)
mov edx, 1 ; EDX = 1 (base value for mask)
mov ebx, edx ; EBX = 1 (copy to working register)
mov ecx, eax ; ECX = i (shift amount)
shl ebx, cl ; EBX = 1 << i (build the bit mask)
mov eax, ebx ; EAX = 1 << i
and eax, [ebp+arg_0] ; EAX = mask & a (test if bit i is set)
test eax, eax ; is EAX zero?
jz short loc_80483EB ; if zero, skip increment
add [ebp+rt], 1 ; rt++
loc_80483EB:
add [ebp+i], 1 ; i++
loc_80483EF:
cmp [ebp+i], 1Fh ; compare i with 31 (0x1F)
jle short loc_80483D0 ; if i <= 31, continue loop
mov eax, [ebp+rt] ; return rt in EAX
add esp, 10h
pop ebx ; restore EBX
pop ebp
retn
f endp

x64

Let's modify the example slightly to extend it to 64-bit:

#include <stdio.h>
#include <stdint.h>
#define IS_SET(flag, bit) ((flag) & (bit)) // macro: tests whether a specific bit is set
int f(uint64_t a)
{
uint64_t i;
int rt = 0; // bit counter
for (i = 0; i < 64; i++) // iterate over all 64 bit positions
if (IS_SET(a, 1ULL << i)) // 1ULL ensures a 64-bit shift
rt++;
return rt;
}

Non-optimizing GCC 4.8.2

Listing 1.294: Non-optimizing GCC 4.8.2

f:
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-24], rdi ; store argument a on stack
mov DWORD PTR [rbp-12], 0 ; rt = 0
mov QWORD PTR [rbp-8], 0 ; i = 0
jmp .L2 ; jump to loop condition
.L4:
mov rax, QWORD PTR [rbp-8] ; RAX = i
mov rdx, QWORD PTR [rbp-24] ; RDX = a
mov ecx, eax ; ECX = i (shift amount)
shr rdx, cl ; RDX = a >> i (shift a right by i bits)
mov rax, rdx ; RAX = a >> i
and eax, 1 ; EAX = (a>>i) & 1 (isolate lowest bit)
test rax, rax ; is lowest bit zero?
je .L3 ; if zero, skip increment
add DWORD PTR [rbp-12], 1 ; rt++
.L3:
add QWORD PTR [rbp-8], 1 ; i++
.L2:
cmp QWORD PTR [rbp-8], 63 ; i <= 63?
jbe .L4 ; if yes, jump back to loop body
mov eax, DWORD PTR [rbp-12] ; rt into EAX for return
pop rbp
ret

Optimizing GCC 4.8.2

Listing 1.295: Optimizing GCC 4.8.2

f:
xor eax, eax ; rt lives in EAX, initialize to 0
xor ecx, ecx ; i lives in ECX, initialize to 0
.L3:
mov rsi, rdi ; load input value (a)
lea edx, [rax+1] ; EDX = EAX + 1 (proposed new value of rt)
shr rsi, cl ; RSI = RSI >> CL (i.e. a >> i)
and esi, 1 ; ESI = (a>>i) & 1 (isolate lowest bit)
cmovne eax, edx ; if ZF=0 (bit was set): EAX = EDX (commit rt++)
add rcx, 1 ; i++
cmp rcx, 64 ; i == 64?
jne .L3 ; if not, repeat loop
rep ret ; FATRET — AMD-recommended return after conditional jump

This code is shorter, but a bit unusual.

In all the examples we have seen so far, we incremented the value of "rt" after comparing a specific bit, but here the code increments "rt" beforehand (line 6), writing the new value into register EDX. This means: if the last bit was 1, the CMOVNE instruction (which is equivalent to CMOVNZ) commits the new value of "rt" by moving EDX ("proposed rt value") into EAX ("the current rt that will be returned").

Consequently, the increment operation happens at every step of the loop — that is, 64 times — regardless of the input value.

The advantage of this code is that it contains only one conditional jump (at the end of the loop) instead of two (one to skip the rt++ increment and one at the end of the loop). This can make it faster on modern processors that have branch predictors: section 2.4.1 on page 575.

The last instruction is REP RET (opcode F3 C3), also called FATRET by MSVC. This is an optimized variant of RET, recommended by AMD to be placed at the end of a function when RET immediately follows a conditional jump.


Optimizing MSVC 2010

Listing 1.296: Optimizing MSVC 2010

a$ = 8
f PROC
; RCX = input value
xor eax, eax ; rt = 0
mov edx, 1 ; RDX = 1 (initial bit mask)
lea r8d, QWORD PTR [rax+64] ; R8D = 64 (loop counter, counts down)
npad 5 ; padding for alignment
$LL4@f:
test rdx, rcx ; is this bit set in the input value?
je SHORT $LN3@f ; if not, skip increment
inc eax ; rt++
$LN3@f:
rol rdx, 1 ; RDX rotated left by 1 (cycles the bit, same as SHL here)
dec r8 ; R8--
jne SHORT $LL4@f ; if R8 != 0, continue loop
fatret 0 ; optimized return (AMD FATRET)
f ENDP

Here the ROL instruction is used instead of SHL; it is actually "rotate left" rather than "shift left", but in this example it works exactly like SHL.

R8 counts down from 64 to 0. It is like i but in reverse. This is a table of some register values during execution:

RDX R8
0x000000000000000164
0x000000000000000263
0x000000000000000462
0x000000000000000861
......
0x40000000000000002
0x80000000000000001

At the end we see the FATRET instruction.


Optimizing MSVC 2012

Listing 1.297: Optimizing MSVC 2012

a$ = 8
f PROC
; RCX = input value
xor eax, eax
mov edx, 1
lea r8d, QWORD PTR [rax+32] ; R8D = 32 (loop runs 32 times, 2 bits per iteration)
npad 5
$LL4@f:
; first pass --------------------------------
test rdx, rcx ; is bit set in input?
je SHORT $LN3@f
inc eax ; rt++
$LN3@f:
rol rdx, 1 ; rotate mask left (advance to next bit)
; second pass --------------------------------
test rdx, rcx ; test the next bit (loop unrolling: 2 bits per iteration)
je SHORT $LN11@f
inc eax ; rt++
$LN11@f:
rol rdx, 1 ; rotate mask left again
; -------------------------------------------
dec r8 ; R8--
jne SHORT $LL4@f ; if R8 != 0, continue
fatret 0
f ENDP

Optimizing MSVC 2012 does almost the same work as MSVC 2010, but somehow it generates two identical loop bodies and the iteration count became 32 instead of 64.

Honestly, it is hard to say why. Perhaps an optimization trick? Perhaps a longer loop body is faster? Either way, this code is shown here to demonstrate that compiler output can sometimes be very strange and illogical — yet it works perfectly.


ARM

ARM + Optimizing Xcode 4.6.3 (LLVM) (ARM mode)

Listing 1.298: Optimizing Xcode 4.6.3 (LLVM) (ARM mode)

MOV R1, R0 ; R1 = input value a (preserve it)
MOV R0, #0 ; R0 = 0 (rt counter)
MOV R2, #1 ; R2 = 1 (base mask value)
MOV R3, R0 ; R3 = 0 (i counter)
loc_2E54:
TST R1, R2, LSL R3 ; set flags based on R1 & (R2 << R3) i.e. a & (1<<i)
ADD R3, R3, #1 ; R3++ (i++)
ADDNE R0, R0, #1 ; if ZF was cleared by TST (bit was set), increment R0 (rt++)
CMP R3, #32 ; compare i with 32
BNE loc_2E54 ; if i != 32, continue loop
BX LR ; return

TST is the same as TEST in x86.

As we said before, there are no separate shift instructions in ARM mode. Instead there are modifiers such as LSL (Logical Shift Left), LSR (Logical Shift Right), ASR (Arithmetic Shift Right), ROR (Rotate Right), and RRX (Rotate Right with Extend), which can be appended to instructions like MOV, TST, CMP, ADD, SUB, RSB.

These modifiers specify how to shift the second operand and by how many bits. So the instruction TST R1, R2, LSL R3 works here as:

R1 ∧ (R2 ≪ R3) ; bitwise AND of R1 with (R2 shifted left by R3 positions)

ARM + Optimizing Xcode 4.6.3 (LLVM) (Thumb-2 mode)

Almost the same thing, but here two instructions LSL.W/TST are used instead of a single TST, because in Thumb mode it is not possible to embed an LSL modifier directly into TST.

MOV R1, R0 ; R1 = input value a
MOVS R0, #0 ; R0 = 0 (rt counter)
MOV.W R9, #1 ; R9 = 1 (base mask)
MOVS R3, #0 ; R3 = 0 (i counter)
loc_2F7A:
LSL.W R2, R9, R3 ; R2 = R9 << R3 i.e. 1 << i (separate shift, required in Thumb)
TST R2, R1 ; set flags based on R2 & R1 i.e. (1<<i) & a
ADD.W R3, R3, #1 ; i++
IT NE ; if-then block: execute next instruction only if NE (ZF=0)
ADDNE R0, #1 ; rt++ (only if bit was set)
CMP R3, #32 ; compare i with 32
BNE loc_2F7A ; if i != 32, continue loop
BX LR ; return

ARM64 + Optimizing GCC 4.9

Let's take the 64-bit example we used before.

Listing 1.299: Optimizing GCC (Linaro) 4.8

f:
mov w2, 0 ; rt = 0
mov x5, 1 ; X5 = 1 (base mask value)
mov w1, w2 ; i = 0
.L2:
lsl x4, x5, x1 ; X4 = X5 << X1 i.e. 1 << i (build 64-bit mask)
add w3, w2, 1 ; W3 = rt + 1 (proposed new rt)
tst x4, x0 ; (1 << i) & a (test if bit i is set in a)
add w1, w1, 1 ; i++
csel w2, w3, w2, ne ; if NE (bit was set): w2 = w3 (rt = new_rt), else w2 = w2 (no change)
cmp w1, 64 ; i < 64?
bne .L2 ; if not done, repeat loop
mov w0, w2 ; rt into W0 for return
ret

The CSEL instruction is "Conditional SELect". It selects one of two values based on the flags set by TST, and copies the selected value into W2, which holds the variable rt.

ARM64 + Non-optimizing GCC 4.9

Listing 1.300: Non-optimizing GCC (Linaro) 4.8

f:
sub sp, sp, #32
str x0, [sp,8] ; store argument "a" in register save area
str wzr, [sp,24] ; rt = 0
str wzr, [sp,28] ; i = 0
b .L2 ; jump to loop condition
.L4:
ldr w0, [sp,28] ; load i
mov x1, 1 ; X1 = 1
lsl x0, x1, x0 ; X0 = X1 << X0 i.e. 1 << i (build mask)
mov x1, x0 ; X1 = 1 << i
ldr x0, [sp,8] ; X0 = a
and x0, x1, x0 ; X0 = (1<<i) & a
cmp x0, xzr ; compare X0 with zero
beq .L3 ; if zero, skip rt++
ldr w0, [sp,24] ; load rt
add w0, w0, 1 ; rt + 1
str w0, [sp,24] ; rt++
.L3:
ldr w0, [sp,28] ; load i
add w0, w0, 1 ; i + 1
str w0, [sp,28] ; i++
.L2:
ldr w0, [sp,28] ; load i
cmp w0, 63 ; i <= 63?
ble .L4 ; if yes, jump to loop body
ldr w0, [sp,24] ; rt into W0 for return
add sp, sp, 32
ret

MIPS

Non-optimizing GCC

f:
; IDA does not know variable names, we assigned them manually:
; rt = -0x10, i = -0xC, var_4 = -4, a = 0
addiu $sp, -0x18 ; allocate 24 bytes on stack
sw $fp, 0x18+var_4($sp) ; save frame pointer
move $fp, $sp ; set frame pointer
sw $a0, 0x18+a($fp) ; store argument a on stack
sw $zero, 0x18+rt($fp) ; rt = 0
sw $zero, 0x18+i($fp) ; i = 0
b loc_68 ; jump to loop condition check
or $at, $zero ; branch delay slot (NOP)
loc_20:
li $v1, 1 ; $v1 = 1 (base mask value)
lw $v0, 0x18+i($fp) ; $v0 = i (shift amount)
or $at, $zero ; load delay slot (NOP)
sllv $v0, $v1, $v0 ; $v0 = 1 << i (SLLV: variable shift amount from register)
move $v1, $v0 ; $v1 = mask
lw $v0, 0x18+a($fp) ; $v0 = a
or $at, $zero ; load delay slot (NOP)
and $v0, $v1, $v0 ; $v0 = a & (1<<i)
beqz $v0, loc_58 ; if zero (bit not set), skip rt++
or $at, $zero ; branch delay slot (NOP)
lw $v0, 0x18+rt($fp) ; load rt
or $at, $zero ; load delay slot (NOP)
addiu $v0, 1 ; rt + 1
sw $v0, 0x18+rt($fp) ; rt++
loc_58:
lw $v0, 0x18+i($fp) ; load i
or $at, $zero ; load delay slot (NOP)
addiu $v0, 1 ; i + 1
sw $v0, 0x18+i($fp) ; i++
loc_68:
lw $v0, 0x18+i($fp) ; load i
or $at, $zero ; load delay slot (NOP)
slti $v0, 0x20 ; $v0 = (i < 32) ? 1 : 0
bnez $v0, loc_20 ; if i < 32, loop back
or $at, $zero ; branch delay slot (NOP)
lw $v0, 0x18+rt($fp) ; return rt
move $sp, $fp ; load delay slot: restore stack pointer
lw $fp, 0x18+var_4($sp) ; restore frame pointer
addiu $sp, 0x18 ; load delay slot: deallocate stack
jr $ra ; return to caller
or $at, $zero ; branch delay slot (NOP)

This code is long: all local variables live in the local stack and are loaded every time they are needed.

The SLLV instruction is "Shift Word Left Logical Variable" — it differs from SLL in that the shift amount in SLL is embedded in the instruction itself (and therefore fixed), whereas SLLV takes the shift amount from a register.

Optimizing GCC

This code is shorter. There are two shift instructions instead of one. Why?

We could replace the first SLLV instruction with an unconditional branch that jumps directly to the second SLLV. But that would be an extra jump instruction in the function, and it is always better to get rid of it.

f:
; $a0 = a
; variable rt will be in $v0:
move $v0, $zero
; variable i will be in $v1:
move $v1, $zero
li $t0, 1
li $a3, 32
sllv $a1, $t0, $v1 ; $a1 = $t0 << $v1 = 1 << i
loc_14:
and $a1, $a0 ; $a1 = a & (1<<i)
; increment i:
addiu $v1, 1
; jump to loc_28 if a&(1<<i) == 0 and increment rt:
beqz $a1, loc_28
addiu $a2, $v0, 1
; if BEQZ was not triggered, store the updated rt in $v0:
move $v0, $a2
loc_28:
; if i != 32, jump to loc_14 and prepare the next shifted value:
bne $v1, $a3, loc_14
sllv $a1, $t0, $v1
; return
jr $ra
or $at, $zero ; branch delay slot, NOP

1.28.6 Conclusion

Just like the << and >> operations in C/C++, the shift instructions in x86 are SHR/SHL (for unsigned values) and SAR/SHL (for signed values).

The shift instructions in ARM are LSR/LSL (for unsigned values) and ASR/LSL (for signed values).

It is also possible to add a shift suffix to some instructions (called "data processing instructions").

Checking a specific bit (known at compile time)

Test if bit 0b1000000 (0x40) is present in a register value:

Listing 1.303: C/C++

if (input & 0x40) ...

Listing 1.304: x86

TEST REG, 40h
JNZ is_set
; bit is not set

Listing 1.305: x86

TEST REG, 40h
JZ is_cleared
; bit is set

Listing 1.306: ARM (ARM mode)

TST REG, #0x40
BNE is_set
; bit is not set

Sometimes, AND is used instead of TEST, but the flags that get set are the same.

Checking a specific bit (determined at runtime)

This is usually done via this C/C++ code snippet (shift the value n bits to the right, then mask the lowest bit):

Listing 1.307: C/C++

if ((value >> n) & 1) ...

This is typically implemented in x86 code as:

Listing 1.308: x86

; REG = input_value
; CL = n
SHR REG, CL
AND REG, 1

Or (shift 1 bit n times to the left, isolate that bit in the input value and test if it is non-zero):

Listing 1.309: C/C++

if (value & (1 << n)) ...

This is typically implemented in x86 code as:

Listing 1.310: x86

; CL = n
MOV REG, 1
SHL REG, CL
AND input_value, REG

Setting a specific bit (known at compile time)

Listing 1.311: C/C++

value = value | 0x40;

Listing 1.312: x86

OR REG, 40h

Listing 1.313: ARM (ARM mode) and ARM64

ORR R0, R0, #0x40

Setting a specific bit (determined at runtime)

Listing 1.314: C/C++

value = value | (1 << n);

This is typically implemented in x86 code as:

Listing 1.315: x86

; CL = n
MOV REG, 1
SHL REG, CL
OR input_value, REG

Clearing a specific bit (known at compile time)

Simply apply an AND operation with the inverted value:

Listing 1.316: C/C++

value = value & (~0x40);

Listing 1.317: x86

AND REG, 0FFFFFFBFh

Listing 1.318: x64

AND REG, 0FFFFFFFFFFFFFFBFh

This effectively causes all bits to be set except one.

ARM in ARM mode has the BIC instruction, which works like the NOT + AND instruction pair:

Listing 1.319: ARM (ARM mode)

BIC R0, R0, #0x40

Clearing a specific bit (determined at runtime)

Listing 1.320: C/C++

value = value & (~(1 << n));

Listing 1.321: x86

; CL = n
MOV REG, 1
SHL REG, CL
NOT REG
AND input_value, REG
Share

If this article helped you, please share it with others!

CH1.28 Manipulating specific bit(s) (part2)
https://v3nn00m.github.io/posts/re4b/chapter1_28_part2/
Author
0xV3n0m
Published at
2026-05-23

Some information may be outdated

Table of Contents