--- title: 利用 RDTSC 量測時脈週期 tags: - Linux - C - OS description: 在進行程式的時間測量時,可以透過 rdtsc 指令取得時脈週期。 rdtsc 會返回 CPU 啟動之後所經歷的時脈週期數量,因此我們可以透過該指令在兩個區段相減過後的值來計算一段時間,例如量測執行一段程式碼所花費的時間。 --- # 利用 RDTSC 量測時脈週期 在進行程式的時間測量時,可以透過 `rdtsc` 指令取得時脈週期。 `rdtsc` 會返回 CPU 啟動之後所經歷的時脈週期數量,因此我們可以透過該指令在兩個區段相減過後的值來計算一段時間,例如量測執行一段程式碼所花費的時間。 在 CPU 啟動之後,會自動累積週期數,並且會將值紀錄在 EDX 和 EAX 暫存器, <mark>EDX 為高位元, EAX 為低位元</mark>。 # EDX 和 EAX 暫存器 暫存器分為 32 位元以及 64 位元暫存器。RDX 和 RAX 皆為 64 位元暫存器,對應的 EDX 與 EAX 則為 32 位元暫存器。32 位元和 64 位元暫存器並不是獨立的暫存器,以 RAX 與 EAX 為例, EAX 佔了 RAX 的低位 32 位元,[因此對 EAX 暫存器修改時也會影響到 RAX 暫存器。](https://stackoverflow.com/questions/44972293/how-is-rax-different-from-eax) ![image](https://hackmd.io/_uploads/SkCyz4qM1e.png) > 參考資料:[Day6 - 為什麼要使用暫存器?](https://ithelp.ithome.com.tw/articles/10241159) # C 語言內嵌組合語言 有時,為了能增進程式的效率,或者在 C 語言當中加入一些組合語言程式碼,我們會採用內嵌的方式,將組合語言內嵌在C語言當中。針對這點,gcc提供了如圖 4.10的內嵌語法,讓我們在 C 語言中可以直接撰寫組合語言。其中,assembler template 為組合語言程式、output operands 為輸出參數、input operands 為輸入參數,而 list of clobbered registers 則指定了被更改的暫存器參數列表。 ```c asm( ; 內嵌起始符號 assembler template ; 組合語言程式 : output operands ; : 輸出參數列表 : input operands ; : 輸入參數列表 : list of clobbered registers ; : 被更改的暫存器列表 ); ``` 在GNU的內嵌組合語言 (assembler template) 中,由於 % 被用作參數 %1, %2, … 的前置字元,因此當遇到暫存器(像是 %eax) 時,必須使用兩個 % 符號 (%%) 作為暫存器的起始標記,因此,在範例 4.24中,原本的 `addl %ebx, %eax` 指令,就成了 `addl %%ebx, %%eax` 這個更冗長的指令。 在範例 4.24的內嵌參數部分, `:"a"(foo), "b"(bar)` 是輸入參數,代表要將 `foo` 變數傳遞給限制條件為 `a` 的暫存器,由於在 IA32 中限制條件為 `a` 的暫存器就是 `eax` ,因此, `foo` 變數將會傳給 `eax` 暫存器。同理, `"b"(bar)` 參數代表將 `bar` 傳給 `ebx` 暫存器。而輸出參數 `:"=a"(foo)` 則是將 `eax` 的結果傳回到 `foo` 變數中。 >參考資料:[組合語言 -- 在 C 語言當中內嵌組合語言](http://ccckmit.wikidot.com/as:inlinec) 範例 4.24 內嵌組合語言的C程式: ```c int main(void) ; ... { ; movl $10, -8(%ebp) ; foo=-8(%ebp)=10 int foo = 10, bar = 15; ; movl $15, -12(%ebp) ; bar=-12(%ebp)=15 ; 輸入參數: asm( ; movl -8(%ebp), %eax ; eax = -8(%ebp)=foo "addl %%ebx,%%eax" ; movl -12(%ebp), %ebx ; ebx = -12(%ebp)=bar :"=a"(foo) ; /APP ; 嵌入的程式 :"a"(foo), "b"(bar) ; addl %ebx,%eax ; eax = eax+ebx ); ; /NO_APP ; 傳出參數 ; movl %eax, -8(%ebp) ; foo=eax printf("foo=%d\n", foo); ; movl -8(%ebp), %eax return 0; ; movl %eax, 4(%esp) } ; movl $LC0, (%esp) ; call _printf ; ... ``` # 取得 RDTSC 值 由於 `rdtsc` 是將數值存入 EDX 與 EAX 暫存器,因此我們可以透過在 C 語言內嵌組合語言的方式來取得暫存器的資料。透過以下實做,會返回 rdtsc 值: ```c= uint64_t rdtsc() { usigned int lo, hi __asm__ volatile ("rdtsc": "=a" (lo), "=d" (hi)); return ((uint64_t)hi << 32) | lo; } ``` 在上述程式碼第 3 行中, `"=a" (lo)` 代表將 `eax` 暫存器的值傳給變數 `lo` , `"=d" (hi)` 代表將 `edx` 暫存器的值傳給變數 `hi` 。由於變數 `lo` 以及 `hi` 分別只存取 `rdtsc` 高位以及低位的 32 位元資料,因此在第 4 行需要將變數 `hi` 先轉型為 64 位元後將再將其值進行左移 32 位元,最後與變數 `lo` 進行 OR 操作即可取得完整的 `rdtsc` 值。 > 參考資料:[你所不知道的 C 語言: bitwise 操作](https://hackmd.io/@sysprog/c-bitwise#Bitwise-Operator) # 一點練習 下列程式碼的目的是輸出 TSC 的時間,在輸出部份為 `: "=m" (msr)` 的情況下,請修正組合語言,讓這個程式能夠將 TSC 的值放入到 `msr` 變數中: > 題目來源:[中正資工:作業系統概論(羅習五教授)](https://www.cs.ccu.edu.tw/~shiwulo/course/2023-osdi/) ```c #include <stdio.h> int main(int argc, char** argv) { unsigned long msr; asm volatile ( "rdtsc\n\t" // Returns the time in EDX:EAX. "shl $32, %%rdx\n\t" // Shift the upper bits left. "or %%rdx, %0" // 'Or' in the lower bits. : "=m" (msr) // msr 會放在記憶體 : : "rdx"); printf("msr: %lx\n", msr); } ``` **解析** 首先在第 4 行會呼叫 `rdtsc` ,因此在 `rdx` 以及 `rax` 會分別存放 `rdtsc` 所回傳的高 32 位元以及低 32 位元資料,在第 5 行首先將 `rdx` 的資料左移 32 位元並且在第 7 行將左移後的資料存入記憶體。因此我們只需要在第 6 行中先將 `rax` 的資料放入記憶體,之後再將 `rdx` 左移後的資料與記憶體內 `rax` 的資料進行 OR 運算,最後將記憶體的資料傳給變數 `msr` 。 ```diff= #include <stdio.h> int main(int argc, char** argv) { unsigned long msr; asm volatile ( "rdtsc\n\t" // Returns the time in EDX:EAX. "shl $32, %%rdx\n\t" // Shift the upper bits left. + "mov %%rax, %0\n\t" // %0 表示第一個輸出操作數,也就是 msr "or %%rdx, %0" // 'Or' in the lower bits. : "=m" (msr) // msr 會放在記憶體 : + : "rdx", "rax"); printf("msr: %lx\n", msr); } ```