Why this matters
A Unix file's permissions, the options you pass to open(), the status register of a device, the header of a TCP packet, the pixel format of an image: all of them pack several small pieces of information into the bits of one integer. To read and write them you need four basic operations (set, clear, toggle, test) and two techniques (extracting a field and inserting one). After this lesson, code like flags & ~O_NONBLOCK or (hdr >> 4) & 0xF will read as plainly as arithmetic.
Masks
A mask is a value whose 1 bits select the positions you care about. A mask for bit n is 1u << n; a mask for the low w bits is (1u << w) - 1 (for example (1u << 4) - 1 is 0b1111 = 0xF).
The four operations, for a single bit n:
set x |= (1u << n) OR with 1 forces the bit on, OR with 0 leaves it
clear x &= ~(1u << n) AND with 0 forces it off, AND with 1 leaves it
toggle x ^= (1u << n) XOR with 1 flips it, XOR with 0 leaves it
test (x >> n) & 1u 1 if the bit is on, 0 if off
or (x & (1u << n)) != 0
The same operations work with masks of many bits at once: x |= 0xF0 sets bits 4 to 7.
Flags
A flag set gives each yes/no option its own bit, so one integer can hold many options. You define the flags as powers of two, often with an enum:
#include <stdio.h>
enum {
PERM_READ = 1u << 0, /* 001 */
PERM_WRITE = 1u << 1, /* 010 */
PERM_EXEC = 1u << 2, /* 100 */
};
void show(unsigned perms)
{
printf("%c%c%c\n",
(perms & PERM_READ) ? 'r' : '-',
(perms & PERM_WRITE) ? 'w' : '-',
(perms & PERM_EXEC) ? 'x' : '-');
}
int main(void)
{
unsigned p = PERM_READ | PERM_WRITE; /* combine with | */
show(p);
p |= PERM_EXEC; /* add one */
show(p);
p &= ~PERM_WRITE; /* remove one */
show(p);
p ^= PERM_READ; /* flip one */
show(p);
return 0;
}
rw-
rwx
r-x
--x
This is exactly how the operating system works. Unix file modes like 0755 are octal because each octal digit is three bits, rwx, for owner, group and others. And the POSIX open call takes flags combined with |: open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644), which you will use in the systems module.
Testing with & gives the masked value, not 0 or 1: p & PERM_EXEC is 4 when set. That is fine in an if, but if you need an actual 1 (to store or compare), write (p & PERM_EXEC) != 0 or !!(p & PERM_EXEC).
Fields: several bits as one number
A field is a group of adjacent bits that together hold a small number. A 24-bit color 0xRRGGBB has three 8-bit fields:
bit: 23 ........ 16 15 ......... 8 7 .......... 0
+--------------+---------------+--------------+
| red (8) | green (8) | blue (8) |
+--------------+---------------+--------------+
0x3366CC: 0x33 0x66 0xCC
Extracting a field is "shift down, then mask": (color >> 8) & 0xFF is green. Inserting fields is "mask, shift up, then OR":
uint32_t color = 0x3366CC;
unsigned r = (color >> 16) & 0xFF;
unsigned g = (color >> 8) & 0xFF;
unsigned b = color & 0xFF;
printf("%u %u %u\n", r, g, b); /* 51 102 204 */
uint32_t back = (r << 16) | (g << 8) | b;
printf("%06X\n", back); /* 3366CC */
Replacing one field inside a word, without disturbing the others, combines everything: clear the field with an inverted mask, then OR in the new value. To put 0x7 into the 4-bit field at bits 8 to 11:
uint32_t reg = 0xABCD1234;
reg = (reg & ~(0xFu << 8)) | (0x7u << 8);
printf("%08X\n", reg); /* ABCD1734 */
reg 1010 1011 1100 1101 0001 0010 0011 0100 ABCD1234
~(0xF << 8) 1111 1111 1111 1111 1111 0000 1111 1111
AND 1010 1011 1100 1101 0001 0000 0011 0100 ABCD1034
0x7 << 8 0000 0000 0000 0000 0000 0111 0000 0000
OR 1010 1011 1100 1101 0001 0111 0011 0100 ABCD1734
Struct bit fields
C can also do the shifting and masking for you. In a struct, a member followed by : width is a bit field that occupies exactly that many bits:
struct pixel {
unsigned red : 5;
unsigned green : 6;
unsigned blue : 5;
};
struct pixel px = {31, 63, 0};
printf("%zu %u %u %u\n", sizeof px, px.red, px.green, px.blue); /* 4 31 63 0 */
The 16 bits of fields fit in one 4-byte unsigned. Assigning a value too big for the field silently keeps only the low bits; gcc warns when it can see it:
warning: unsigned conversion from 'int' to 'unsigned char:6' changes value from '64' to '0' [-Woverflow]
Bit fields are convenient for flags inside your own program's data. But the order of bit fields within a word, and the padding between them, are implementation-defined, so do not use them to match an external format (a file header, a network packet, a hardware register): the explicit shifts and masks above say exactly where every bit goes on every compiler. You also cannot take the address of a bit field.
Common mistakes
flags & !MASKinstead offlags & ~MASK.!is logical NOT and gives 0, wiping every bit.- Forgetting to clear before inserting.
word | (value << shift)only adds 1 bits; the old field's 1 bits stay. Clear with& ~maskfirst. - Not masking the inserted value. If
valuehas more bits than the field, the extra bits spill into the neighbouring field.(value << shift) & maskprevents it. - Signed masks.
~(1 << n)is fine for smalln, but1 << 31is UB. Use1u. (1u << 32) - 1for a full 32-bit mask. Shifting by the full width is UB. Use0xFFFFFFFFuorUINT32_MAX, and keep field widths below 32 in general code.
Recap
- Set with
|=, clear with&= ~, toggle with^=, test with&(or shift and& 1). - Flags are powers of two combined with
|; that is how the OS passes options. - Extract a field with
(x >> shift) & mask; replace one with(x & ~(mask << shift)) | ((v & mask) << shift). - Struct bit fields are handy for internal data but their layout is implementation-defined; use explicit masks for external formats.
Going further: run stat -c '%a %A' /etc/passwd in the sandbox terminal to see a mode as octal and as letters, and look at man 2 open for the flag names. Then read /usr/include/x86_64-linux-gnu/bits/fcntl-linux.h to see that O_CREAT and friends are just numbers with single bits set.