利用 RDTSC 量測時脈週期

在進行程式的時間測量時,可以透過 rdtsc 指令取得時脈週期。 rdtsc 會返回 CPU 啟動之後所經歷的時脈週期數量,因此我們可以透過該指令在兩個區段相減過後的值來計算一段時間,例如量測執行一段程式碼所花費的時間。

在 CPU 啟動之後,會自動累積週期數,並且會將值紀錄在 EDX 和 EAX 暫存器, EDX 為高位元, EAX 為低位元

EDX 和 EAX 暫存器

暫存器分為 32 位元以及 64 位元暫存器。RDX 和 RAX 皆為 64 位元暫存器,對應的 EDX 與 EAX 則為 32 位元暫存器。32 位元和 64 位元暫存器並不是獨立的暫存器,以 RAX 與 EAX 為例, EAX 佔了 RAX 的低位 32 位元,因此對 EAX 暫存器修改時也會影響到 RAX 暫存器。
image

參考資料:Day6 - 為什麼要使用暫存器?

C 語言內嵌組合語言

有時,為了能增進程式的效率,或者在 C 語言當中加入一些組合語言程式碼,我們會採用內嵌的方式,將組合語言內嵌在C語言當中。針對這點,gcc提供了如圖 4.10的內嵌語法,讓我們在 C 語言中可以直接撰寫組合語言。其中,assembler template 為組合語言程式、output operands 為輸入參數、input operands 為輸入參數,而 list of clobbered registers 則指定了被更改的暫存器參數列表。

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 語言當中內嵌組合語言

範例 4.24 內嵌組合語言的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 值:

1
2
3
4
5
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 操作

一點練習

下列程式碼的目的是輸出 TSC 的時間,在輸出部份為 : "=m" (msr) 的情況下,請修正組合語言,讓這個程式能夠將 TSC 的值放入到 msr 變數中:

題目來源:中正資工:作業系統概論(羅習五教授)

#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);
}

解析

首先在第四行會呼叫 rdtsc ,因此在 rdx 以及 edx 會分別存放 rdtsc 所回傳的高 32 位元以及低 32 位元資料,在第 5 行首先將 rdx 的資料左移 32 位元並且在第 7 行將左移後的資料存入記憶體。因此我們只需要在第 6 行中先將 rax 的資料放入記憶體,之後再將 rdx 左移後的資料與記憶體內 rax 的資料進行 OR 運算,最後將記憶體的資料傳給變數 msr

1
2
3
4
5
6
7
8
9
10
11
12
#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"
"or %%rdx, %0" // 'Or' in the lower bits.
: "=m" (msr) //msr 會放在記憶體
:
+ : "rdx", "rax");
printf("msr: %lx\n", msr);
}