--- title: '位元操作' disqus: kyleAlien --- 位元操作 === ## OverView of Content 越接近底層(越靠近硬體),我們越常使用到位元運算,所以位元操作必須學習 (順便練練你的邏輯) 位元也就是 Byte (8 個 Bit),C 語言中大多數的 **位元運算都是透過 `define` 宏去定義**(省呼叫函數的開銷) :::success * 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/) 本篇文章對應的是 [**理解C語言中的位元操作:位元運算基礎與宏定義**](https://devtechascendancy.com/bitwise-operations-and-macros-in-c/) ::: [TOC] ## 運算基礎:位元、邏輯、位移 1. C 語言有提供幾個操作符號來完成位元的運算 | 符號 | 記憶名稱 | 英文 | | -------- | -------- | - | | & | 與 | AND | | \| | 或 | OR | | ^ | 位異 | XOR | | ~ | 反向 | NOT | 2. 上面這幾種運算還有在分為 **無符號、有符號** 的位移操作 | 符號差異 + 位移 | 名稱 | | -------- | -------- | | 無符號 >> | 右位移 | | 無符號 << | 左位移 | | 有符號 >> | 右位移 | | 有符號 << | 左位移 | * 這裡寫一個簡單的 `cal.sh` Shell 檔,方便之後在虛擬中斷機上計算結果 ```shell= #!/bin/bash cal() { # 參數 1, 10 進制轉為 2 進制 x1=$(echo "ibase=10;obase=2;$1" | bc) # 參數 3, 10 進制轉為 2 進制 x2=$(echo "ibase=10;obase=2;$3" | bc) case $2 in ">>"|"<<") x2=$3 ;; esac # 以下是顯示、計算步驟 # 顯示轉換二進位的計算 echo "1. change to binary:" echo " $x1 $2 $x2" # 進行計算 echo "2. cal" x3=$(echo "$[$1 $2 $3]") # 將結過轉為二進制 x4=$(echo "ibase=10;obase=2;$x3" | bc) echo " $x4" # 將十進位結果輸出 echo "3. change to decimal" result=$(echo "$x3") echo " $result" } cal $1 $2 $3 exit 0 ``` :::info Shell 的數學運算請參考「[**建構腳本 - 基礎、數學運算**](https://hackmd.io/G0Q9-UMVSeO7wNpMcaUwvw?view#Bash-shell-----%E7%AC%A6%E8%99%9F)」 ::: ### 「與」運算 `&` * 首先我們看看 `&` 運算規則,如下 **真值表** | & | 0 | 1 | | -------- | -------- | -------- | | 0 | 0 | 0 | | 1 | 0 | 1 | * `&` 就是 `並`,如其名,將每個數轉為 2 進位分開比較,**要兩個數是 1 才是 1** 1. 題目:`5 & 9` ```c= # 1. 先轉為 2 進位 0101 & 1001 # 2. 每個位元相對應做 & 運算 0101 1001 ------ 0001 # 轉為 10 進位 5 & 9 = 1 ``` > ![](https://i.imgur.com/5v8ecIt.png) 2. 題目:`12 & 9` ```c= # 1. 先轉為 2 進位 1100 & 1001 # 2. 每個位元相對應做 & 運算 1100 1001 ------ 1000 # 轉為 10 進位 12 & 9 = 8 ``` > ![](https://i.imgur.com/1yfwy9B.png) ### 「與邏輯」運算 `&&` * 邏輯 `&&` 要將要比較的兩個數看作一個整體(不用分開比較),只要 **該數 ++不等於 0 就是 1++(==負數也算是 1==)**,兩個數都要大於 0 就成立 1. 題目:`1 && 9` 結果為 True ```c= 1 != 0 => true 9 != 0 => true ``` > ![](https://i.imgur.com/rfoJH8Y.png) 2. 題目:`1 && -1` 結果為 True ```c= 1 != 0 => true -1 != 0 => true ``` > ![](https://i.imgur.com/2wqpd9g.png) 3. 題目:`1 && 0` 結果為 Flase ```c= 1 != 0 => true 0 != 0 => false ``` > ![](https://i.imgur.com/UQNWn3M.png) ### 「或」運算 `|` * 首先我們看看 `&` 運算規則,如下 **真值表** | \| | 0 | 1 | | -------- | -------- | -------- | | 0 | 0 | 1 | | 1 | 1 | 1 | * `|` 就是 `或`,如其名,將每個數轉為 2 進位分開比較,**只要有一個數是 1 那就是 1** 1. 題目:`8 & 9` ```c= # 1. 先轉為 2 進位 1000 & 1001 # 2. 每個位元相對應做 | 運算 1000 1001 ------ 1001 # 轉為 10 進位 8 | 9 = 9 ``` > ![](https://i.imgur.com/DcRO9Tt.png) 2. 題目:`12 & 9` ```c= # 1. 先轉為 2 進位 1100 & 1001 # 2. 每個位元相對應做 | 運算 1100 1001 ------ 1000 # 轉為 10 進位 12 | 9 = 13 ``` > ![](https://i.imgur.com/i4whsGW.png) ### 「或邏輯」 `||` * 邏輯 `||` 要將要比較的兩個數看作一個整體(不用分開比較),只要 **該數 ++不等於 0 就是 1++(負數也是 1)**,兩個數一個大於 0 就成立 1. 題目:`12 || 9` 結果為 True ```c= 12 != 0 => true 9 != 0 => true ``` > ![](https://i.imgur.com/mQtJv8i.png) 2. 題目:`12 || 0` 結果為 True ```c= 12 != 0 => true 0 != 0 => false ``` > ![](https://i.imgur.com/JyYcmvU.png) 3. 題目:`0 && 0` 結果為 Flase ```c= 0 != 0 => false 0 != 0 => false ``` > ![](https://i.imgur.com/PbKRcGY.png) ### 「取反」運算 `~` * 簡單來說就是讓二進制位元反轉,像是… 1 轉 0、0 轉 1 ```c= ~0 = 1 ~1 = 0 ``` 1. 題目:`~1101` ```c= ~1101 = 0010 // (2) ``` 2. 題目:`~1001` ```c= ~1001 = 0110 // (6) ``` ### Xor 運算 `^` * 首先我們看看 `Xor` 運算規則,這種 Xor 運算很常使用在加法器的進位運算,如下 **真值表** | ^ | 0 | 1 | | -------- | -------- | -------- | | 0 | 0 | 1 | | 1 | 1 | 0 | > Shell 中使用 Xor 的運算符為為 `^` * `Xor` 就是 `位異`,如其名,將每個數轉為 2 進位分開比較,**只要有一個數是 1 那就是 1,但若 ++==兩個都是 1 那則會變為 0==++** 1. 題目:`12 ^ 11` ```c= # 1. 先轉為 2 進位 1100 & 1011 # 2. 每個位元相對應做 ^ 運算 1100 1011 ------ 0111 # 轉為 10 進位 12 ^ 11 = 7 ``` > ![](https://i.imgur.com/c3ncTZ0.png) 2. 題目:`6 & 9` ```c= # 1. 先轉為 2 進位 0110 & 1001 # 2. 每個位元相對應做 | 運算 0110 1001 ------ 1111 # 轉為 10 進位 6 | 9 = 15 ``` > ![](https://i.imgur.com/i4whsGW.png) ### 「左位移」運算 `<<` * 將計算數字轉為 2 進位,並向左邊位移,並且位移過後補 0 (位移完總不能空著位子吧 ~) 1. 題目:`6 << 1` ```c= # 1. 先轉為 2 進位 0110 << 1 # 2. 每個位元相對應做 << 位移 1100 # 轉為 10 進位 6 << 1 = 12 ``` > ![](https://i.imgur.com/dI8iYnM.png) 2. 題目:`6 << 9` ```c= # 1. 先轉為 2 進位 0110 << 9 # 2. 每個位元相對應做 << 位移 110000000000 # 轉為 10 進位 6 << 9 = 3072 ``` > ![](https://i.imgur.com/KXtlJeo.png) :::success * **左移 `<<` 特性**: 每進行一次左移,就會是原本數字的「底數倍數」 > `x << 左位移數` = x * 底數^左位移數^ 我們拿二進制來看,二進制的底數為 `2`,每經過一次左移,就會是原本數字的 2 倍 > `x << n` = x * 2^n^ > 以下計算 6 的位移 > > ![](https://i.imgur.com/KCxk6N8.png) ::: ### 「右位移」運算 `>>` (2 的補數應用) * 將計算數字轉為 2 進位,並向右邊位移,位移過後仍需要補足,而 右位移 **補足有兩個狀況**,如下表 | ➡️ 右移捕的數字 | 可運用在 | | - | - | | 右補 0 | `無符號` or `有符號正數` | | 右補 1 | `有符號負數` | 1. 題目:正數 `6 >> 1` ```c= # 1. 先轉為 2 進位 0110 >> 1 # 2. 每個位元相對應做 >> 位移 11 # 轉為 10 進位 6 >> 1 = 3 ``` > ![](https://i.imgur.com/r5mn0Rc.png) 2. 題目:正數 `15 >> 3` ```c= # 1. 先轉為 2 進位 1111 >> 3 # 2. 每個位元相對應做 >> 位移 1 # 轉為 10 進位 15 >> 3 = 1 ``` > :::success * 左移 `<<` 特性 每進行一次左移,就會是原本數字 / 2^n^ > x << n = x / 2^n^ ::: * 負數(這裡自己手動算) * 題目:**負數** `(-15) >> 3` 1. 首先使用 **二的補數計算** 負數的 2 進位 ```c= // 先取 15 的正數 2 進位 15 => 0000 1111 // 將其反向 ~(0000 1111) => (1111 0000) // 將結果 +1 就是 -15 的二進位 1111 0001 ``` 2. 負數位移要用 1 來補 (正數就用 0 來補) ```c= # 1. 先轉為 2 進位 (1111 0001) >> 3 # 2. 每個位元相對應做 >> 位移 3 次 1111 1000 1111 1100 1111 1110 # 位移完結果 ``` 3. 反向 2 的補數取得 10 進位 ```c= # 反向 ~(1111 1110) => (0000 0001) # 結果 減1 0000 0000 # 最終結果加上負號 0 ``` * 題目:`(-5) >> 2` 1. 首先使用 **二的補數計算** 負數的 2 進位 ```c= // 先取 15 的正數 2 進位 5 => 0000 0101 // 將其反向 ~(0000 0101) => (1111 1010) // 將結果 +1 就是 -15 的二進位 1111 1011 ``` 2. 負數位移要用 1 來補 (正數就用 0 來補) ```c= # 1. 先轉為 2 進位 (1111 1011) >> 2 # 2. 每個位元相對應做 >> 位移 2 次 1111 1101 1111 1110 # 位移完結果 ``` 3. 反向 2 的補數取得 10 進位 ```c= # 反向 ~(1111 1110) => (0000 0001) # 結果 減1 0000 0000 # 最終結果加上負號 0 ``` ## 位元:位操作 位元的位操作,常使用在對 Register 的設置操作上 ### 特定位置:清零 `&` * 要指定某幾個 bit 位清零,最常用的是使用 `&` 運算 ```shell= int a = 0xAAAAAA; # 如果希望 8 bit ~ 15 bit 清零 a &= 0xFF00FF # 結果 a: 0xAA00AA ``` ### 特定位置:置為 1 `|` * 要指定某幾個 bit 位置為 1,最常用的是使用 `|` 運算 ```shell= int a = 0xAAAAAA; # 如果希望 8 bit ~ 15 bit 置為 1 a |= 0x00FF00 # 結果 a: 0xAAFFAA ``` ### 特定位置 - 反向 `^` * 要指定某幾個 bit 位反向(與原先的設定相反),最常用的是使用 `^` 運算 ```shell= int a = 0xAAAAAA; # 如果希望 8 bit ~ 15 bit 反向 a ^= 0x00FF00 # 結果 a: 0xAA55AA ``` ### 建構指定二進位 * 通常透過左位移,我們就可以快速建構出我們要的二進位 1. bit3 ~ bit7 為 1 ```c= # 1. bit3 ~ bit7 所有的 1 就是 0x1F # 2. 位移到最低位 (bit3) int result = 0x1F << 3; ``` 2. bit3 ~ bit7 為 1 && bit23 ~ bit25 ```c= # 1. bit3 ~ bit7 所有的 1 就是 0x1F # bit23 ~ bit25 所有的 0x07 # 2. 位移到最低位 (bit3) int result = (0x1F << 3) | (0x07 << 23); ``` * 如果位移的 1 過多,不如就使用 `~` 反向 1. bit4 ~ bit10 為 0 其他都為 1 ```c= # 過多 1 位移 (0x1fffff << 11) | (0x0f < 0) # ---------------------------------------------------- # 1. bit4 ~ bit10 所有的 1 就是 0x7F # 2. ++反向++再位移到最低位 (bit4) int result = (~0x7F) << 4; ``` ### 位元練習 1. bit7 ~ bit17 賦予 654 ```c= // 首先將 bit7 ~ bit17 至為 0 a = a & ~(0x7FF << 7) // 654 位移到最低位 做 or a = a | (654 << 7) ``` 2. 「bit7 ~ bit17 賦予 666」、「bit21 ~ bit25 賦予 99」 ```c= // 首先將 bit7 ~ bit17、bit21 ~ bit25 至為 0 a = a & ~((0x7FF << 7) | (0x1F << 21)) // 654 位移到最低位 做 or a = a | ((666 << 7) | (99 << 21)) ``` ## 宏定義 - 位元 Linux 內核中有許多宏定義來協助開發者使用,接下來我們拿幾個宏來舉例 :::info 在 `define` 中的運算都需要使用 `()`,避免預編譯時造成運算錯誤 ::: ### 宏定義 - 指定位為 1 1. **宏模型**:`#define SET_BIT_N(x, n) xxx` > x: 計算目標 > n: 第幾位制為 1 2. **宏模型實現**:`#define SET_BIT_N(x, n) ((x) | ( 1 << (n - 1) ))` ### 宏定義 - 指定為復位 0 1. **宏模型**:`#define CLS_BIT_N(x, n) xxx` > x: 計算目標 > n: 第幾位制為 0 2. **宏模型實現**:`define CLS_BIT_N(x, n) ((x) & ~( 1 << (n - 1) ))` ### 宏定義 - 擷取指定位元 * 假設我們有一個數為 0x66(轉換為二進制就是`0b01100110`),那這個數的 bit6 ~ bit2 就是 `0b10011` 1. **宏模型**:`define GET_BITES (x, n, m) xxx` 2. **宏模型實現**:`define GET_BITES (x, n, m) ((x & ~(\~(0U) << (m-n+1))<<(n+1)) >> (n-1))` > x: 計算目標 > n: 開始位 > m: 結束位 * 宏模型的實現步驟分析 1. **要先從最內部開始分析**:就是 `~(0U) << (m-n+1)` > eg. 取 0x66 的 bit6 ~ bit2 > 就是 GET_BITES(0x66, 2, 6) > > x=0x66, n=2, m=6 > > ~(0U) << (6-2+1) > Temp ans1: 1111 1111 1111 1111 1111 1111 1110 0000 2. **取正確數量的數據**:反向 `~(~(0U) << (m-n+1))` > ~(Temp ans1) > > Temp ans2: 0000 0000 0000 0000 0000 0000 0001 1111 3. **移動到正確位置**:左位移 `~(~(0U) << (m-n+1))<<(n+1)` > (Temp ans2) << (2+1) > > Temp ans3: 0000 0000 0000 0000 0000 0000 1111 1000 4. **取得數據**:`x & ~(~(0U) << (m-n+1))<<(n+1)` > Temp ans4: 0x66 & Temp3 > Temp ans4: 0x66 & 0xF8 > Temp ans4: 0x60 5. **位移回正常位置** > Final Ans: Temp ans4 >> (n+1) > Final Ans: 0x60 >> (3) = 0b0000 1010 > Final Ans: 10 ## 更多的 C 語言相關文章 關於 C 語言的應用、研究其實涉及的層面也很廣闊,但主要是有關於到系統層面的應用(所以 C 語言又稱之為系統語言),為了避免文章過長導致混淆重點,所以將文章係分成如下章節來幫助讀者更好地從不同的層面去學習 C 語言 ### C 語言基礎 * **C 語言基礎**:有關於到 C 語言的「語言基礎、細節」 :::info * [**理解C語言中的位元操作:位元運算基礎與宏定義**](https://devtechascendancy.com/bitwise-operations-and-macros-in-c/) * [**C 語言解析:void 意義、NULL 意義 | main 函數調用、函數返回值意義 | 臨時變量的產生**](https://devtechascendancy.com/meaning_void_null_return-value_temp-vars/) * [**C 語言中的 Struct 定義、初始化 | 對齊、大小端 | Union、Enum**](https://devtechascendancy.com/c-struct_alignment_endianness_union_enum/) * [**C 語言儲存類別、作用域 | 修飾語、生命週期 | 連結屬性**](https://devtechascendancy.com/c-storage-scope-modifiers-lifecycle-linkage/) * [**指標 & Array & typedef | 指標應用的關鍵 9 點 | 指標應用、細節**](https://devtechascendancy.com/pointers-arrays-const-typedef-sizeof-null/) ::: ### 編譯器、系統開念 * **編譯器、系統開念**:是學習完 C 語言的基礎(或是有一定的程度)之後,從編譯器以及系統的角度重新檢視 C 語言的一些細節 :::warning * [**理解電腦記憶體管理 | 深入瞭解記憶體 | C 語言程式與記憶體**](https://devtechascendancy.com/computer-memory_manager-c-explained/) * [**C 語言記憶體區塊規劃 | Segment 段 | 字符串特性**](https://devtechascendancy.com/c-memory-segmentation-string-properties/) * [**編譯器的角度看程式 | 低階與高階、作業系統、編譯器、直譯器、預處理 | C語言函數探討**](https://devtechascendancy.com/compiler-programming-os-c-functions/) ::: ### C 語言與系統開發 * **C 語言與系統開發**:在這裡會說明 C 語言的實際應用,以及系統為 C 語言所提供的一些函數、庫... 等等工具,看它們是如何實現、應用 :::danger * [**了解 C 語言函式庫 | 靜態、動態函式庫 | 使用與編譯 | Library 庫知識**](https://devtechascendancy.com/understanding-c-library-static-dynamic/) * [**Linux 宏拓展 | offsetof、container_of 宏、鏈表 | 使用與分析**](https://devtechascendancy.com/linux-macro_offsetof_containerof_list/) ::: ## Appendix & FAQ :::info ::: ###### tags: `C`