---
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)

> 參考資料:[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);
}
```