contributed by < leewei05
>
linux2022
移動平均(Moving average),又稱滾動平均值、滑動平均,在統計學中是種藉由建立整個資料集合中不同子集的一系列平均數,來分析資料點的計算方法。
移動平均通常與時間序列資料一起使用,以消除短期波動,突出長期趨勢或周期。短期和長期之間的閾值取決於應用,移動平均的參數將相應地設置。例如,它通常用於對財務數據進行技術分析,如股票價格、收益率或交易量。它也用於經濟學中研究國內生產總值、就業或其他宏觀經濟時間序列。
這個直覺的解法會有 overflow 的問題,若我們已知 a, b 數值的大小,可用下方程式避免 overflow:
接著我們可改寫為以下等價的實作:
下列程式碼 a 與 b 分別先除 2 再相加,當兩個數值皆為奇數時平均數會少加 1,故透過 a & b & 1
來判斷是否把少加的 1 加回去。
根據數值系統的 bit-wise operator 筆記,運用加法器的邏輯思考。兩數的和為 a ^ b
,兩數的進位為 (a & b) << 1
,可得 a + b = a ^ b + (a & b) << 1
。
EXP2
為 a & b
,EXP3
為 a ^ b
。
環境:Ubuntu 20.04.3 LTS
編譯器版本:gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
gdb 反組譯結果,或是可以透過 gcc -Og -S avg.c
輸出組合語言。gcc 預設是匯出 ATT 格式, Intel 格式的則是可以透過 -masm=intel
來產生。
閱讀 Intel IA-32 規格書 以及 CS:APP 第三章並試著解讀以下的組合語言指令。首先分析 avg.s
的內容,LFB
以及 LFE
分別是 gcc 編譯時根據 DWARF(除錯輸出格式) 所產生的 label (.L
為 local label),分別代表函式的開始以及結束。
Local symbols are defined and used within the assembler, but they are normally not saved in object files. Thus, they are not visible when debugging.
因為 gcc -S
只會編譯 (compile) 程式碼並輸出組合語言,尚未進行組譯 (assemble) 以及連結 (link),所以可以直接看到組合語言用到的 local label。
.cfi
代表 DWARF 所定義的 Call Frame Instruction(CFI)
:
Debuggers often need to be able to view and modify the state of any subroutine activation that is on the call stack. An activation consists of:
• A code location that is within the subroutine. This location is either the place where the
program stopped when the debugger got control (e.g. a breakpoint), or is a place where a
subroutine made a call or was interrupted by an asynchronous event (e.g. a signal).
• An area of memory that is allocated on a stack called a ‘‘call frame.’’ The call frame is
identified by an address on the stack. We refer to this address as the Canonical Frame
Address or CFA.
• A set of registers that are in use by the subroutine at the code location.
根據以上的描述,可以理解成 CFI 為一個子程序 (subroutine) 的起始或是結束觸發點。一個配置於 stack memory 的記憶體區塊稱作 call frame
,而此 call frame
的記憶體位址我們稱作 Canonical Frame Address(CFA)
。回到下方的組合語言,.cfi-startproc
以及 .cfi-endproc
分別是組合語言中定義的 CFI directives (指令),代表著函式的起始以及結束。
.cfi_startproc is used at the beginning of each function that should have an entry in .eh_frame. It initializes some internal data structures. Don’t forget to close the function by .cfi_endproc.
在了解 endbr64
指令以前,需要先知道 ROP 以及 COP/JOP 這兩項攻擊手法。黑客可以藉由上述提到的手法竄改原先的指令(e.g. RET, CALL, JUMP)來操作既有的指令。而 Intel 處理器提供的 Control-Flow Enforcement Technology (CET) 則是用來防範上述提到的攻擊,以下是 CET 所提供的兩項主要功能:
endbr64
則是隸屬於 Indirect branch tracking 的指令。
18.1.2 Indirect Branch Tracking
The
ENDBRANCH
instruction is a new instruction that is used to mark valid jump target addresses of indirect calls and jumps in the program. … The CPU implements a state machine that tracks indirectJMP
andCALL
instructions. When one of these instructions is executed, the state machine moves fromIDLE
toWAIT_FOR_ENDBRANCH
state. InWAIT_FOR_ENDBRANCH
state the next instruction in the program stream must be anENDBRANCH
. If the next instruction is not anENDBRANCH
, the processor causes a control protection exception (#CP); otherwise, the state machine moves back toIDLE
state.
接著我們看 average 函式當中的指令。mov
延伸問題:
請在 Linux 核心原始程式碼中,找出更多類似的實作手法。請善用 git log 檢索。
改寫〈解讀計算機編碼〉一文的「不需要分支的設計」一節提供的程式碼 min,我們得到以下實作 (max):
EXP4 為 a 和 b 進行某種特別 bitwise 操作,限定用 OR, AND, XOR 這三個運算子
EXP5 為 a 和 b 的比較操作,亦即 logical operator,限定用 >, <, ==, >=, <=, !=
((EXP4) & -(EXP5))
會需要等於 0,因為 a ^ 0 = a
((EXP4) & -(EXP5))
會需要是 b - a
6.5.8 Relational operators
Each of the operators < (less than), > (greater than), <= (less than or equal to), and >=
(greater than or equal to) shall yield 1 if the specified relation is true and 0 if it is false.92)
The result has type int.
根據上述規格書的內容,EXP5
只會返回 1 或是 0。
EXP5 == 1
,則可以得到 ((EXP4) & -1)
,亦等於 ((EXP4) & 0xFFFF)
EXP5 == 0
,則可以得到 ((EXP4) & 0x0000)
。從第二個條件回推,任何數跟 0 做 AND 運算會等於 0,條件二即符合 max = a
。根據現有條件,可以推測出 EXP5
為 a <= b
,如果 a 等於 b 那直接回傳 a 也合理。但題目有說到以最精簡的方式撰寫程式碼,那其實 a < b
即可。EXP4
為 a ^ b
也等同於 b - a
,任何數跟 0xFFFF
做 AND 運算都會返回自己。
完整程式碼
任何一個變數為 0 則返回另一個非零的變數。如果兩個變數皆為偶數則兩個變數皆除以二,並且把除以二的次數記錄到 shift
。
如果 u
為偶數則除以二。 do ... while
的迴圈裡頭則是實作 Euclid's algorithm。根據 Euclid's algorithm 的描述可以推斷COND
為 u != v && u && v
,RET
為 u << shift
。最後需要把一開始兩邊皆除以二的次數乘回 u
才會是最大公因數。
COND = v
不需要 u != v 因為如果 u == v,COND = v 會直接跳出 while loop。而 COND = v 是因為在做輾轉相除法時,只要當 u - v 為 0 時就會跳出 while loop。
__builtin_ctz
改寫 GCDBuilt-in Function: int __builtin_ctz (unsigned int x)
Returns the number of trailing 0-bits in x, starting at the least significant bit position. If x is 0, the result is undefined.
Built-in Function: int __builtin_ctzll (unsigned long long)
Similar to __builtin_ctz, except the argument type is unsigned long long.
根據 gcc 官方文件的描述,可以透過 __builtin_ctz
來取得無號數 x
的 trailing 0 bits,換句話說即等於 shift
的值。而原本題目給定的變數是 unsigned long long
,即替換 __builtin_ctz
為 __builtin_ctzll
。
其中第 9 行的作用是找出目前最低位元的 1,並紀錄到 t 變數。舉例來說,若目前 bitset 為 ,那 t 就會變成 ,接著就可以透過 XOR 把該位的 1 清掉,其他保留 (此為 XOR 的特性)。
請補完程式碼。書寫規範:
考慮到 -bitset
會對 bitset
做二的補數,透過下列程式範例理解:
EXP6
bitset & -bitset
-bitset & bitset
真實案例:Bloomfilter, Compression
LeetCode 166. Fraction to Recurring Decimal
給定 numerator (分子)以及 denominator (分母),返回小數的字串。如果遇到重複的小數迴圈,則把重複的部分以括號 ()
表示。
從建立的 Hash table 中找尋特定的 key。
分母為零,則直接返回 \0
(null terminated string);分子為零,則返回 0
。雖然在 Leetcode 題目中有提到輸入的分母並不會為 0,但保險起見,還是檢查分母是否為零以免出現非預期行為。
Constraints
- -2^31 <= numerator, denominator <= 2^31 - 1
- denominator != 0
根據 C 語言規格書,第二個運算元如果為 0 則此行為是 undefined
。
The result of the / operator is the quotient from the division of the first operand by the
second; the result of the % operator is the remainder. In both operations, if the value of
the second operand is zero, the behavior is undefined.
6.5.3.3 Unary arithmetic operators
The result of the unary - operator is the negative of its (promoted) operand. The integer
promotions are performed on the operand, and the result has the promoted type.
Promote 在此解釋為 型別提升。例如 float, double, long double
皆為浮點數,假設 float
跟 double
做運算,需要先對 float
做型別提升,否則運算的結果會不如預期,因為 float
跟 double
所佔有的位元組空間不同。
6.5.3.4 The sizeof and _Alignof operators
The sizeof operator yields the size (in bytes) of its operand, which may be an
expression or the parenthesized name of a type
判別小數為正數或是負數。p
為 pointer to char,在 x84_64 位元的儲存空間為 8 bytes。
6.5.3.2 Address and indirection operators
The unary * operator denotes indirection. If the operand points to a function, the result is
a function designator; if it points to an object, the result is an lvalue designating the
object. If the operand has type ‘‘pointer to type’’, the result has type ‘‘type’’.
根據以上敘述,p
為 pointer to char type 則 *p
為 char type。如果 sign
為負數,則把 '-'
寫入至第一個 char slot,即 result[0]
,並且把 p
指標指向 result[1]
。
可以透過下方的小程式了解其中的關係:
可以看到輸出結果如我們預期,一開始 p
是指向 result[0]
。*p++
是指派 a
至 result[0]
,再指派 p
的指標至下一個 char
的起始位置。
計算除完的結果 (division) 以及餘數 (remainder),如果整除則直接返回 division。
7.19.6.6 The sprintf function
2 The sprintf function is equivalent to fprintf, except that the output is written into
an array (specified by the argument s) rather than to a stream. A null character is written
at the end of the characters written; it is not counted as part of the returned value. If
copying takes place between objects that overlap, the behavior is undefined.
3 The sprintf function returns the number of characters written in the array, not
counting the terminating null character, or a negative value if an encoding error occurred.
6.5.2.1 Array subscripting
A postfix expression followed by an expression in square brackets [] is a subscripted
designation of an element of an array object. The definition of the subscript operator []
is that E1[E2] is identical to (*((E1)+(E2))). Because of the conversion rules that
apply to the binary + operator, if E1 is an array object (equivalently,apointer to the
initial element of an array object) and E2 is an integer, E1[E2] designates the E2-th
element of E1 (counting from zero).
sprintf
將會把 division
寫入至 p (pointer to char)
現在的位置,也就是 result[0]
(正數)或是 result[1]
(負數),並再最後加入 .
字元。
初始化 hash table 的各個 hash slots。
可以判斷如果 pos
小於 0 則表示沒有在 hash table 找到 remainder
,所以需要把目前 remainder
的值加入至目前 hash slot 所處的 linked list 中。而 find()
中的 hash function 為 int hash = key % size;
根據上述的描述,可以推測 MMM
為 list_add
,而 EEE
為 &heads[remainder % size]
。
如果 pos 有值則表示為有循環小數,所以需要先把循環小數前的數寫到 p
,故 PPP = pos--
。
ALIGNOF(t)
即為計算出 type t 的 alignment。
6.3.2.3 Pointers
An integer may be converted to any pointer type. Except as previously specified, the
result is implementation-defined, might not be correctly aligned, might not point to an
entity of the referenced type, and might be a trap representation.5
下列相減要得出 type t 的 alignment。兩項相減需要為 type t 的 alignment,故 X 為 0。
可以透過下列實驗得證,為什麼 dereference type t 的記憶體位置相減會得到 type t 的 alignment。
–
以下是利用 bitwise 和上述技巧實作的 FizzBuzz 程式碼: (bitwise.c)
KK1 = div5
, KK2 = div3
, KK3 = div3 << 2