ASM Logic Operations

Elucian MoiseElucian Moise
13 min read

A logic operation is a Boolean operation that performs logical computations on Boolean variables (variables that can only have two values: true or false, 1 or 0, etc).

The main types of logic operations are:

  • AND: Returns true if and only if both operands are true.

  • OR: Returns true if either of the operands is true.

  • NOT: Reverses the logical state of its operand.

  • XOR: Returns true if one of the operands is true, but not both.

For example:

  • A AND B

  • If A is true and B is true, then AND returns true.

  • Otherwise, it returns false.

Logic operations are used to combine conditions, make decisions, and perform bitwise operations. They form the basis of digital logic and circuits like logic gates.

So in essence, a logic operation performs a logical computation on Boolean variables to determine if the resulting output should be true or false based on the given operands and operations.


Compared to C

Assembly language logic operations are essentially equivalent to bitwise operations in other languages like C. Here are some key points:

  • Logic operations in Assembly like AND, OR, XOR, NOT operate on individual bits of operands, not boolean values.

  • This means they are effectively performing bitwise operations, manipulating the bits directly.

  • In languages like C, we have separate logic operators (&&, ||, !) for boolean operations, and bitwise operators (&, |, ^, ~) for bit manipulation.

  • But in Assembly, the same instructions (AND, OR, XOR, NOT) are used for both logical and bitwise purposes. There is no distinction.

  • So while logic operations in Assembly are conceptually similar to logical operations in other languages, technically they are performing bitwise operations under the hood.

  • This gives the Assembly more control over manipulating individual bits, which is necessary at the low-level of Assembly.

  • But in higher-level languages, the distinction between logical and bitwise operations is made to abstract away the bit manipulation and focus on boolean logic.

So in summary, yes you're correct - logic operations in Assembly are effectively equivalent to bitwise operations in languages like C. The difference is mainly conceptual and syntactic, while technically they are both manipulating bits. The distinction is made in higher-level languages for abstraction.


In Assembly

Here is a summary of logic operations in Assembly language:

OperationExampleDescription
ANDAND AL, BLPerforms a bitwise AND operation on the operands AL and BL and stores the result in AL. Returns 1 only if both bits are 1.
OROR AL, BLPerforms a bitwise OR operation on the operands AL and BL and stores the result in AL. Returns 1 if either bit is 1.
XORXOR AL, BLPerforms a bitwise exclusive OR operation on the operands AL and BL and stores the result in AL. Returns 1 if the bits are different (only one bit is 1).
NOTNOT ALPerforms a bitwise NOT (logical complement) on the operand AL.

Logic operations in Assembly are different in the following ways:

  • They operate on individual bits instead of boolean values.

  • They manipulate the bits in registers directly.

  • They use specific instructions (AND, OR, XOR, NOT) to perform the operations.

  • They provide more control over how the operations are performed.

In higher-level languages, logic operations are often implemented as functions that operate on Boolean values: (True/False). But in Assembly, we have to work at the bit level using the specific logic operation instructions.


Bit Manipulation

Here are some common bit-level manipulation instructions in Assembly language, other than logic operations:

  • Shift Left (SHL): Moves all bits of a value to the left, filling the vacated bit positions with zeros. This effectively multiplies the value by 2.

  • Shift Right (SHR): Moves all bits of a value to the right, filling the vacated bit positions with the original sign bit. This effectively divides the value by 2.

  • Rotate Left (ROL): Similar to shift left, but the vacated bit is moved to the other end. This rotates all bits to the left by one position.

  • Rotate Right (ROR): Similar to shift right, but the vacated bit is moved to the other end. This rotates all bits to the right by one position.

These shift and rotate instructions allow you to:

  • Double or halve a value quickly

  • Extract specific bits from a value

  • Manipulate bits for flags and status registers

  • Implement bitwise operations

For example:

MOV AL, 1 ; Store 1 in AL
SHL AL ; Shift left by 1, AL now contains 10 (binary)

Some other bit-level instructions:

  • BSF: Finds first set bit (1 bit) in a value and stores its position

  • BSR: Finds the last set bit (1 bit) in a value and stores its position

  • BSWAP: Swaps the high and low order bytes of a value

These instructions give you fine-grained control over individual bits, which is necessary for low-level programming in Assembly.


Use-Cases

Here are some common use cases where bit manipulation and logic operations can provide performance improvements or unique advantages:

  1. Checking multiple conditions efficiently: Instead of checking each condition separately, you can combine them using logic operations and check them at once. This can be faster than multiple if statements.

  2. Compressing data: By packing multiple bits of data into a single byte or word, you can compress data and reduce memory usage. Bit manipulation is essential for this.

  3. Implementing bitwise operations: Bit manipulation instructions allow you to implement bitwise AND, OR, XOR, NOT, etc. operations, which have many uses like encrypting/decrypting data, bit twiddling, etc.

  4. Optimizing loop conditions: Instead of incrementing a loop counter by 1 each time, you can increment by 2, 4, 8, etc. using shift operations. This can speed up loops.

  5. Flags and status registers: Many CPUs have flags (bits) that indicate status like zero, carry, sign, overflow, etc. Logic operations are used to set/clear these flags.

  6. Interfacing with hardware: Many devices communicate via specific bit patterns. Bit manipulation allows you to interface with these devices efficiently.

  7. Memory optimization: Storing multiple boolean values in a single byte/word and accessing them via bit manipulation can reduce memory usage.

Shift and Rotate

Here are some common use cases for shift and rotate operations:

Shift Left:

  • Multiply by 2: Shifting left by 1 bit effectively doubles a value. This can be faster than multiplication.

  • Multiply by powers of 2: Shifting left by n bits multiplies a value by 2^n.

  • Extract specific bits: By shifting right then left, you can extract a range of bits from a value.

  • Implement bitwise operations: Shifting is used to implement bitwise operations like AND, OR, XOR.

Shift Right:

  • Divide by 2: Shifting right by 1 bit halves a value. This can be faster than division.

  • Divide by powers of 2: Shifting right by n bits divides a value by 2^n.

  • Sign extension: Shifting right a signed value fills the left bits with the sign bit, extending the sign.

Rotate:

  • Cyclic shift: Rotating a value shifts all bits in a circular fashion.

  • Implementing rotation cipher: Used in encryption to implement rotation ciphers.

  • Masking and filtering: Rotating can be used to filter out specific bits of a value.

  • Bit twiddling: Rotating bits can implement techniques like setting, clearing and toggling bits.

So in summary, the main uses of shift and rotate operations are:

  • Multiplying and dividing values by powers of 2 quickly

  • Extracting or masking specific bits

  • Sign extension for signed values

  • Implementing bitwise and encryption operations

  • Bit twiddling and filtering techniques

XOR Tricks

Here are some interesting use cases for the XOR operation in Assembly language:

  1. Encryption: XOR is commonly used in simple encryption schemes due to its properties. If you XOR a value with a key, then XOR it again with the same key, and you get the original value back.

For example, a simple encryption:

Key = 0x5A 
Value = 0xAB
Encrypted = Value XOR Key = 0xAB XOR 0x5A = 0x4D
Decrypted = Encrypted XOR Key = 0x4D XOR 0x5A = 0xAB

So XOR can be used to reversibly encrypt/decrypt data with a key.

  1. Parity bit generation: XOR can be used to generate a parity bit that indicates whether the total number of 1 bits in a value is odd or even.

For example, to generate an even parity bit for 0101:

0101 (5 in binary)
XOR 0  -> 0101 (odd number of 1s, so parity bit is 1)
Result: 01011 (even parity)
  1. Checksum generation: XOR can be used to generate checksums for error detection. The checksum is the XOR of all bytes in a block of data. The receiver then XORs all bytes again and compares it to the checksum. If they match, no errors occurred.

  2. Setting, clearing and toggling bits: XOR a value with 1 to set a bit, 0 to clear a bit, and the current bit value to toggle that bit.

For example, to toggle the 4th bit:

01001100   (Original)
XOR 00001000   (Toggle 4th bit)
=> 01001110 (4th bit toggled)

In summary, XOR has many useful properties that make it ideal for simple encryption, parity checks, checksums, and bit manipulation - all tasks commonly needed in low-level Assembly programming.

Another interesting use-case for XOR

You can clear registry keys very fast using the XOR operation in registry manipulation APIs.

When you XOR a value with 0, it remains the same. But when you XOR it with itself, the result is 0.

So to clear a registry value using XOR, you can do the following:

  1. Get the existing registry value using RegGetValue()

  2. XOR that value with itself

  3. Set the registry value using the XOR'd result, using RegSetValueEx()

Since XOR'ing a value with itself results in 0, this effectively sets the registry value to 0.

For example:

// Get existing registry value 
DWORD curValue;  
RegGetValue(.., .., .., RRF_RT_REG_DWORD, &curValue, ..);

// XOR curValue with itself
DWORD clearValue = curValue ^ curValue;  

// Set registry value to the XOR'd result  
RegSetValueEx(.., .., .., RRF_RT_REG_DWORD, (LPBYTE)&clearValue, ..);

This is much faster than simply setting the registry value to 0, since it only requires 2 API calls instead of 3:

  1. GetValue

  2. XOR

  3. SetValue

So in summary, by XOR'ing a registry value with itself, you can clear that registry value very quickly with only 2 API calls, instead of the usual 3 calls needed.


AL & BL Registers

When working with bit manipulation is good to have a good understanding of registry notation and what role they play. Some registries are smaller but they can be part of a larger registry. For example:

AL and BL are part of larger registers AX and BX respectively. This means:

  • AL is the low byte (least significant byte) of AX, which is a 16-bit register

  • BL is the low byte of BX, which is also a 16-bit register

So when you access data in AL or BL:

  • If the data is 8 bits or less, it is stored only in AL or BL.

  • The high byte of AX or BX is unused/unaffected in this case.

But if the data is 16 bits:

  • The low byte of the data will be stored in AL or BL

  • The high byte will be stored in AH or BH, the high byte of AX or BX.

So accessing 8-bit data in AL/BL only affects that register. But accessing 16-bit data in AX/BX:

  • Uses AL/BL to store the low byte

  • Uses AH/BH to store the high byte

This allows AX and BX to effectively act as 16-bit registers, while AL and BL can be used independently as 8-bit registers when needed.

The CPU accesses AL/BL directly when you read or write 8-bit data to those registers. But when reading/writing 16-bit data to AX/BX, the CPU actually accesses both the low byte (AL/BL) and the high byte (AH/BH) registers.

AL and BL are not necessarily "favorite" registers for logical operations. They do have some advantages though:

  1. They are 8-bit registers, which is suitable for many logical operations that operate on 8-bit quantities.

  2. They are part of the AX and BX register pairs, which are some of the most commonly used registers. Having data in AL and BL makes it convenient to use the corresponding 16-bit AX and BX registers if needed.

  3. They are among the fastest registers to access on many processors, since they are part of the accumulator register set.

However, there are also some drawbacks to using AL and BL for logical operations:

  1. They have limited 8-bit size, so they cannot be used for logical operations on larger data types (>8 bits). You would need to use 16- or 32-bit registers for that.

  2. They have specific purposes in x86 architecture - AL is used for string instructions while BL is used for loop counters. Using them for logical operations can interfere with these roles.

  3. Other registers like EAX, EBX, ECX, EDX are general purpose and can be used freely for logical operations on any size data.

AL and BL are best used for their intended roles in string and loop operations, leaving the larger registers to handle general logical operations.

So in summary, AL and BL are the low byte portions of the 16-bit AX and BX registers respectively. When accessing 8-bit data, only AL and BL are used. But for 16-bit access, both the low byte and high byte registers are involved.


Small Registers

The x86 architecture defines registers of different sizes: 8-bit, 16-bit, 32-bit and 64-bit. The smaller registers are actually subsets of the larger registers.

For example:

  • The AL register is an 8-bit register that contains the lowest byte (bits 0-7) of the 16-bit AX register.

  • The AX register contains the lowest 16 bits (bits 0-15) of the 32-bit EAX register.

  • The EAX register contains the lowest 32 bits (bits 0-31) of the 64-bit RAX register.

So the registers are hierarchical:

RAX | EAX | AX | AL

This means:

  • When you access AL, you are only accessing or modifying the lowest 8 bits of RAX. The rest of the bits in RAX are unaffected.

  • When you access AX, you are accessing or modifying the lowest 16 bits of RAX, consisting of:

    • The 8 bits in AL

    • The next 8 bits in AH (the high byte of AX)

  • And so on as you go up the hierarchy. Accessing EAX affects bits 0-31 of RAX, and accessing RAX affects all 64 bits.

The benefit of this hierarchical design is that:

  • You can access the minimum register size needed for your operation, optimizing performance.

  • You can still access the larger registers and their full range of bits when needed.


Larger Registers

The same pattern repeats for registers larger than 16 bits. There are low and high registers for both 32-bit and 64-bit registers:

32-bit registers:

  • EAX - Consists of AL (low byte) and AH (high byte)

  • EBX - Consists of BL and BH

  • ECX - Consists of CL and CH

  • EDX - Consists of DL and DH

64-bit registers:

  • RAX - Consists of EAX (low doubleword) and EDX (high doubleword)

  • RBX - Consists of EBX and EDX

  • RCX - Consists of ECX and EDX

  • RDX - Consists of EDX itself

So in summary:

  • The 16-bit registers consist of a low byte and high byte

  • The 32-bit registers consist of a low doubleword (4 bytes) and high byte

  • The 64-bit registers consist of a low doubleword and high doubleword (4 bytes each)

This allows you to:

  • Access the minimum number of bits needed for a given operation

  • Preserve the lower bits while modifying the higher bits

The same pattern repeats - the lower registers contain a subset of the bits from the higher registers. This hierarchical register design is an optimization for efficient data access and manipulation.


Disclaim: This article was generated with AI. I'm not an expert but I make an effort to understand Assembly. You can help if you post questions below. I may ask more questions about this topic and respond.

0
Subscribe to my newsletter

Read articles from Elucian Moise directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Elucian Moise
Elucian Moise

Software engineer instructor, software developer and community leader. Computer enthusiast and experienced programmer. Born in Romania, living in US.