contributed by <ian910297
>
騰訊所開發的 soter ,讓 app 開發者可以快速實現安全的指紋驗證。
實現安全性的原因
架構
TENCENT SOTER 中,有三個級別的密鑰:ATTK, App Secure Key(ASK) 以及 AuthKey。這些都是 RSA-2048 的非對稱密鑰。
所有的密鑰都由 TEE(一個獨立於Android系統的安全環境,這也是 TENCENT SOTER 能解決 root 下手機認證的關鍵所在)保護安全保存。這也就意味著除了數據所有者之外,沒有人可以 使用私鑰。更重要的是,如果密鑰是在 TEE 中生成的,所有人 - 包括微信,廠商等 - 都無法得到密鑰私鑰。一句話總結,TENCENT SOTER 中所有非對稱密鑰均是在 TEE 內部生成的,即使設備被根,私鑰也不會洩露。
除了密鑰之外,TENCENT SOTER 所有關鍵流程,如簽名等,均在 TEE 中進行,這也保證了核心流程不會被惡意應用篡改。
更詳細的流程流程可以參閱 Github Wiki - TENCENT SOTER原理
詳閱 Moxie 處理器架構的 Architecture 文件,了解指令架構
在看專案前,要先了解一下 libffi
概念:假設我們有一個 pointer 指向我們想使用的 function ,還需要知道輸入、輸出的數值跟型態。我們不會直接接觸到那個 function ,都透過 ffi 所提供的介面來操控。
ffi_cif
代表 Call InterFace ,用來符合 function signature
ffi_prep_cif
初始化,在準備用來呼叫的物件
ffi_call
搭配初始化過後的 ffi_cif 來做使用,即可成功 call function
#include <stdio.h>
#include <ffi.h>
int main()
{
ffi_cif cif;
ffi_type *args[1];
void *values[1];
char *s;
ffi_arg rc;
/* Initialize the argument info vectors */
args[0] = &ffi_type_pointer;
values[0] = &s;
/* Initialize the cif */
if (ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 1,
&ffi_type_sint, args) == FFI_OK)
{
s = "Hello World!";
ffi_call(&cif, puts, &rc, values);
/* rc now holds the result of the call to puts */
/* values holds a pointer to the function's arg, so to
call puts() again all we need to do is change the
value of s */
s = "This is cool!";
ffi_call(&cif, puts, &rc, values);
}
return 0;
}
binutils
待補
gcc
gcc 裡面的 gcj 有使用到 ffi 這個 library ,GCJ 是在 interpreted 和 natively compiled 程式碼之間溝通做使用 ,不過 gcj 在 gcc 7 時已經被從專案移除掉,所以選擇 gcc-6.4.0
來做研究
libjava/link.cc
運用到 closure 包裝
void *
_Jv_Linker::create_error_method (_Jv_Utf8Const *class_name, jclass klass)
{
void *code;
method_closure *closure
= (method_closure *)ffi_closure_alloc (sizeof (method_closure), &code);
closure->arg_types[0] = &ffi_type_void;
// Initializes the cif and the closure. If that worked the closure
// is returned and can be used as a function pointer in a class'
// atable.
if ( ffi_prep_cif (&closure->cif,
FFI_DEFAULT_ABI,
1,
&ffi_type_void,
closure->arg_types) == FFI_OK
&& ffi_prep_closure_loc (&closure->closure,
&closure->cif,
_Jv_ThrowNoClassDefFoundErrorTrampoline,
class_name,
code) == FFI_OK)
{
closure->list.registerClosure (klass, closure);
return code;
glibc
待補
qemu
待補
參考資料
關鍵特性
基於 Intel CPU Instruction 去實作的 TEE
讓開發者可以將 App 切為兩個部分,可以把關鍵私密的資料都儲存在 Trusted Part ,且無法從指定的 call function 之外的地方對 Trusted Part 內容進行存取
應用案例 - Azure confidential computing
使用兩套 TEE: VSM (software-based), Intel SGX (hardware-based)
參考資料
詳閱文件 sandbox execution environment
待補
開發環境未必會等於使用環境,使開發出的程式可以在特定環境底下執行
以 moxiebox/tests/basic
為例
使用 moxie-none-moxiebox-readelf -a
來讀取 ELF 檔案,會得到 ELF Header
, Section Header
, Program Header
以及 Symbol table
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Moxie
Version: 0x1
Entry point address: 0x1000
Start of program headers: 52 (bytes into file)
Start of section headers: 28200 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 2
Size of section headers: 40 (bytes)
Number of section headers: 22
Section header string table index: 21
除了使用 moxie-none-moxiebox-readelf
也可以藉由 moxie-none-moxiebox-objdump
來列出 ELF 執行擋有哪些 section,列出的項目會不太一樣
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00001000 000074 000452 00 AX 0 0 2
[ 2] .init PROGBITS 00001452 0004c6 00000e 00 AX 0 0 2
[ 3] .fini PROGBITS 00001460 0004d4 000008 00 AX 0 0 2
[ 4] .rodata PROGBITS 00001468 0004dc 000004 00 A 0 0 4
[ 5] .data PROGBITS 0000156c 0004e0 000430 00 WA 0 0 4
[ 6] .eh_frame PROGBITS 0000199c 000910 000004 00 WA 0 0 4
[ 7] .ctors PROGBITS 000019a0 000914 000008 00 WA 0 0 4
[ 8] .dtors PROGBITS 000019a8 00091c 000008 00 WA 0 0 4
[ 9] .bss NOBITS 000019b0 000924 000028 00 WA 0 0 4
[10] .comment PROGBITS 00000000 000924 00003c 01 MS 0 0 1
[11] .debug_aranges PROGBITS 00000000 000960 0000f0 00 0 0 1
[12] .debug_info PROGBITS 00000000 000a50 0031bb 00 0 0 1
[13] .debug_abbrev PROGBITS 00000000 003c0b 000cbe 00 0 0 1
[14] .debug_line PROGBITS 00000000 0048c9 000dd1 00 0 0 1
[15] .debug_frame PROGBITS 00000000 00569c 000178 00 0 0 4
[16] .debug_str PROGBITS 00000000 005814 000a1b 01 MS 0 0 1
[17] .debug_loc PROGBITS 00000000 00622f 000415 00 0 0 1
[18] .debug_ranges PROGBITS 00000000 006644 000038 00 0 0 1
[19] .symtab SYMTAB 00000000 00667c 0004d0 10 20 49 4
[20] .strtab STRTAB 00000000 006b4c 000214 00 0 0 1
[21] .shstrtab STRTAB 00000000 006d60 0000c7 00 0 0 1
.text
執行的程式碼.init
在進入 main 之前所執行的程式碼.fini
程式正常結束後所需執行的程式碼.rodata
唯獨的資料,不可寫入.data
有初始化的資料.ctors
constructor 相關資訊.dtors
deconstructor 相關資訊.bss
未初始化的資料,這邊的資料沒有檔案空間,所以 section type 是 NOBIT
.comment
版本控制相關資訊.debug_XXX
debug 相關的資訊.symtab
控制 symbol table.strtab
控制跟 symbol table 有關的字串.shstrtab
控制 section nameProgram Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x00000f8c 0x00000f8c 0x004e0 0x004e0 R E 0x1
LOAD 0x0004e0 0x0000156c 0x0000156c 0x00444 0x0046c RW 0x1
Section to Segment mapping:
Segment Sections...
00 .text .init .fini .rodata
01 .data .eh_frame .ctors .dtors .bss
Symbol table '.symtab' contains 77 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00001000 0 SECTION LOCAL DEFAULT 1
2: 00001452 0 SECTION LOCAL DEFAULT 2
3: 00001460 0 SECTION LOCAL DEFAULT 3
4: 00001468 0 SECTION LOCAL DEFAULT 4
5: 0000156c 0 SECTION LOCAL DEFAULT 5
6: 0000199c 0 SECTION LOCAL DEFAULT 6
7: 000019a0 0 SECTION LOCAL DEFAULT 7
參考資料
待補
參考資料
.text
和 .data
section 內容呢?以 moxiebox/tests/basic
為例,使用 moxie-none-moxiebox-gdb
進行除錯
使用 info files
取得 section address
(gdb) info files
Symbols from "/home/demonic/sandbox/moxiebox/tests/basic".
Local exec file:
`/home/demonic/sandbox/moxiebox/tests/basic', file type elf32-littlemoxie.
Entry point: 0x1000
0x00001000 - 0x00001452 is .text
0x00001452 - 0x00001460 is .init
0x00001460 - 0x00001468 is .fini
0x00001468 - 0x0000146c is .rodata
0x0000156c - 0x0000199c is .data
0x0000199c - 0x000019a0 is .eh_frame
0x000019a0 - 0x000019a8 is .ctors
0x000019a8 - 0x000019b0 is .dtors
0x000019b0 - 0x000019d8 is .bss
使用 x
(for "examine memory")
(gdb) x/5i 0x1000
0x1000 <__start>: xor $fp, $fp
0x1002 <__start+2>: gsr $sp, 0x7
0x1004 <__start+4>: ldi.l $r0, 0x19b0
0x100a <__start+10>: xor $r1, $r1
0x100c <__start+12>: ldi.l $r2, 0x19d8
(gdb) x/5i 0x156c
0x156c <_impure_ptr>: mul.x $r5, $fp
0x156e <_impure_ptr+2>: bad
0x1570 <impure_data>: bad
0x1572 <impure_data+2>: bad
0x1574 <impure_data+4>: bad
參考資料
~> ps -aef | grep a.out
demonic 9201 8370 3 19:28 pts/0 00:00:00 gdb a.out
demonic 9203 9201 0 19:28 pts/0 00:00:00 /home/demonic/test/a.out
demonic 9207 9203 0 19:28 pts/0 00:00:00 /home/demonic/test/a.out
demonic 9210 9090 0 19:28 pts/9 00:00:00 grep --color=auto a.out
~> gdb -q a.out
(gdb) attach 9207
Attaching to program: /home/demonic/test/a.out, process 9207
Could not attach to process. If your uid matches the uid of the target
process, check the setting of /proc/sys/kernel/yama/ptrace_scope, or try
again as the root user. For more details, see /etc/sysctl.d/10-ptrace.conf
ptrace: Operation not permitted.
解決辦法:
編輯 /etc/sysctl.d/10-ptrace.conf
將其數值改為 0,並且使用 sysctl -p /etc/sysctl.d/10-ptrace.conf
更新 /proc/sys/kernel/yama/ptrace_scope
的數值,如此即可繼續練習使用 gdb 。也可以只更動 proc 底下的數值,但重新開機就會需要重新設定
遠端除錯
target remote 部份,已使用 moxie-none-moxiebox-gdb
進行練習
target cisco 部份,沒有機器可以試用
core dump 除錯
為了產生 coredump 檔案,須先執行 ulimited -c unlimited
參考資料
啟動 sandbox
~> ../src/sandbox -e basic -g 9999
使用 netstat
進行觀察,發現在 0.0.0.0:9999
的位置上,已經開啟一個 tcp 連線
~> netstat -tcpl
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:902 0.0.0.0:* LISTEN -
tcp 0 0 localhost:mysql 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:hostmon 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:9999 0.0.0.0:* LISTEN 11680/../src/sandbo
實作程式碼在 src/sandbox.cc
static int gdb_main_loop(uint32_t &gdbPort, machine &mach)
{
int sockfd, newsockfd, on;
struct sockaddr_in serv_addr, cli_addr;
socklen_t clilen;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("ERROR opening socket");
return EXIT_FAILURE;
}
on = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (char *) &on, sizeof(on));
on = 1;
setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, (char *) &on, sizeof(on));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(gdbPort);
if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) {
close(sockfd);
perror("ERROR on binding");
return EXIT_FAILURE;
}
listen(sockfd, 1);
clilen = sizeof(cli_addr);
newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen);
老師在這部分還預留了兩個 bug 以及一個 TODO
待補
啟動 moxie-none-moxiebox-gdb
~> moxie-none-moxiebox-gdb -q basic
(gdb) target remote :9999
待補
待補
參考資料
contributed by <st9007a
>
cmbc 是一個用於 C/C++ 的 Bounded Model Checker,主要來檢測程式碼中是否存在潛在不合法的陣列或指標使用,更詳細的資訊可以使用指令 cbmc --help
查看:
$ cbmc --help
Program instrumentation options:
--bounds-check enable array bounds checks
--pointer-check enable pointer checks (always enabled for Java)
--memory-leak-check enable memory leak checks
--div-by-zero-check enable division by zero checks
--signed-overflow-check enable signed arithmetic over- and underflow checks
--unsigned-overflow-check enable arithmetic over- and underflow checks
--pointer-overflow-check enable pointer arithmetic over- and underflow checks
--conversion-check check whether values can be represented after type cast
--undefined-shift-check check shift greater than bit-width
--float-overflow-check check floating-point for +/-Inf
--nan-check check floating-point for NaN
--no-built-in-assertions ignore assertions in built-in library
--no-assertions ignore user assertions
--no-assumptions ignore user assumptions
--error-label label check that label is unreachable
--cover CC create test-suite with coverage criterion CC
--mm MM memory consistency model for concurrent programs
其主要檢測項目有陣列 , 指標 , 記憶體跟 overflow 等等
參考 Applications of CBMC,cbmc 在這裡主要用於除錯或偵測潛在的錯誤,再參考 使用事件自動機規約的C语言有界模型檢測,在一些複雜且不容出錯或者難以除錯的環境中,如交通跟醫療,可以透過形式化驗證來檢測系統
一個有限自動機中有以下特徵:
以上圖來舉例,A, B, C 就是所有的狀態,0, 1 則是輸入,箭頭代表輸入後狀態的轉變,其中:
轉移函數則可以用表格來表示:
0 | 1 | |
---|---|---|
A | A | B |
B | A | C |
C | C | C |
最左邊的 column 代表現在的狀態,對應到給定的輸入後轉變的狀態
待補
跟著 CBMC: Bounded Model Checking for C/C++ and Java 操作,首先對以下程式執行指令 cbmc test1.c --bounds-check --pointer-check
int puts(const char *s) { }
int main(int argc, char **argv) {
puts(argv[2]);
}
可以看到以下輸出
** Results:
[main.pointer_dereference.1] dereference failure: pointer NULL in argv[(signed long int)2]: SUCCESS
[main.pointer_dereference.2] dereference failure: pointer invalid in argv[(signed long int)2]: SUCCESS
[main.pointer_dereference.3] dereference failure: deallocated dynamic object in argv[(signed long int)2]: SUCCESS
[main.pointer_dereference.4] dereference failure: dead object in argv[(signed long int)2]: SUCCESS
[main.pointer_dereference.5] dereference failure: pointer outside dynamic object bounds in argv[(signed long int)2]: SUCCESS
[main.pointer_dereference.6] dereference failure: pointer outside object bounds in argv[(signed long int)2]: FAILURE
** 1 of 6 failed (2 iterations)
VERIFICATION FAILED
從輸出訊息中注意到 pointer outside object bounds in argv[(signed long int)2]: FAILURE
,接著利用 --show-properties
可以查看每個要驗證的 property:
Property main.pointer_dereference.6:
file test1.c line 4 function main
dereference failure: pointer outside object bounds in argv[(signed long int)2]
16l + POINTER_OFFSET(argv) >= 0l && OBJECT_SIZE(argv) >= 24ul + (unsigned long int)POINTER_OFFSET(argv) || DYNAMIC_OBJECT(argv)
上面為 --show-properties
的其中一個 property,而這個 property 的 rule 就是最後一行的 16l + POINTER_OFFSET(argv) >= 0l && OBJECT_SIZE(argv) >= 24ul + (unsigned long int)POINTER_OFFSET(argv) || DYNAMIC_OBJECT(argv)
而這個 rule 來自於 cbmc 自己的規定,再來可以用 --trace
來查看 fail 發生的原因,它會在 fail 的 property 中 trace 失敗發生時的前幾個狀態
Trace for main.pointer_dereference.6:
State 18 thread 0
----------------------------------------------------
INPUT argc: 1 (00000000000000000000000000000001)
State 19 thread 0
----------------------------------------------------
argv'[1]=((char *)NULL) (0000000000000000000000000000000000000000000000000000000000000000)
State 22 file test1.c line 3 thread 0
----------------------------------------------------
argc=1 (00000000000000000000000000000001)
State 23 file test1.c line 3 thread 0
----------------------------------------------------
argv=argv' (0000001000000000000000000000000000000000000000000000000000000000)
Violated property:
file test1.c line 4 function main
dereference failure: pointer outside object bounds in argv[(signed long int)2]
16l + POINTER_OFFSET(argv) >= 0l && OBJECT_SIZE(argv) >= 24ul + (unsigned long int)POINTER_OFFSET(argv) || DYNAMIC_OBJECT(argv)
從 trace 的結果可以大概知道當 argc = 1 時,在第 4 行程式碼會發生違反 property 的狀況,而第 4 行剛好是
puts(argv[2]);
也就是 argc = 1 時會發生存取到超過 argv 的有效範圍的位置
進入 moxiebox/runtime
中,可以發現裡面有幾個跟記憶體操作有關的程式碼,打開其中的 memchr.c
它實際上執行的程式碼如下:
void *memchr(const void *src_void, int c, size_t length)
{
const unsigned char *src = (const unsigned char *) src_void;
unsigned char d = c;
while (length--) {
if (*src == d)
return (void *) src;
src++;
}
return NULL;
}
它會從 src_void
為起點,往後 length
的長度中找到符合 c
的 character
針對這個 function 做 unsigned integer overflow 的檢查,會出現驗證失敗的狀況:
$ cbmc memchr.c --function memchr --unwind 10 --unsigned-overflow-check
...
** 1 of 1 failed (1 iteration)
VERIFICATION FAILED
在 cbmc 中加入 --trace
查看詳細的原因:
$ cbmc memchr.c --function memchr --unwind 10 --unsigned-overflow-check --trace
...
State 105 file memchr.c line 120 function memchr thread 0
----------------------------------------------------
length=0ul (0000000000000000000000000000000000000000000000000000000000000000)
State 108 file memchr.c line 123 function memchr thread 0
----------------------------------------------------
src=&tmp$1!0 (0000001000000000000000000000000000000000000000000000000000001001)
Violated property:
file memchr.c line 120 function memchr
arithmetic overflow on unsigned - in length - 1ul
!overflow("-", size_t, length, 1ul)
** 1 of 1 failed (1 iteration)
VERIFICATION FAILED
研究了一下,其原因出在當 length = 0 時,除了跳出 while 迴圈以外還會將 length 再減一次 1,由於 length 型態為 unsigned int 因此這裡就出現 unsigned integer overflow,以這個案例來說,這樣的實作方式其實不會造成什麼危害,要注意到的是 cbmc 找到的不一定是 bug,也有可能是潛在的漏洞
修正方法:
while (length) {
length--;
if (*src == d)
return (void *) src;
src++;
}
return NULL;
為防止發生 0ul - 1 的狀況,在迴圈中才做 length--
的操作,利用 cbmc 再次確認:
$ cbmc memchr.c --function memchr --unwind 10 --unsigned-overflow-check
...
VERIFICATION SUCCESSFUL
待補
Applications of CBMC
使用事件自動機規約的C语言有界模型檢測
[Coursera][Automata] 自動機理論-Automata筆記-第一週Finite Automata
自動機理論
What is model checking
contributed by <st9007a
>
full-stack-hello 這個 repo 中的 virtual machine 使用 direct threading 的方法來實作 opcode dispatch,direct threading 是在一個 function 內設置多個 label,透過一個 direct threading table 來決定每個 opcode 所要前往的 label,在這個 repo 中 vm_run
一開始使用 BEGIN_OPCODES
所產生的 label list 就是用來扮演 direct threading table 的腳色
這邊將比較兩種 opcode dispatch 方式的效能,這兩種方式分別是 computed goto 跟 switch
首先在 full-stack-hello 開另一個 branch 實作 switch statement 版本的 virtual machine:
void vm_run(vm_env *env)
{
while (1) {
switch (OPCODE.opcode) {
case OP(ADD): {
VM_CALL_HANDLER();
break;
}
case OP(SUB): {
VM_CALL_HANDLER();
break;
}
...中間略...
case OP(CLZ): {
VM_CALL_HANDLER();
break;
}
case OP(JLT): {
VM_JLT();
break;
}
case OP(JLE): {
VM_JLE();
break;
}
case OP(JGT): {
VM_JGT();
break;
}
case OP(JGE): {
VM_JGE();
break;
}
case OP(JZ): {
VM_JZ();
break;
}
case OP(JNZ): {
VM_JNZ();
break;
}
case OP(JMP): {
profiling();
GOTO(OPCODE.op1.value.id);
break;
}
case OP(CALL): {
VM_CALL(OPCODE.op1.value.id);
break;
}
case OP(RET): {
VM_RET();
break;
}
case OP(HALT): {
profiling();
END_OPCODES;
#if defined(profile)
show_profiling_info();
#endif
return;
}
}
}
}
macro 的部分也要做一點修改,主要是改掉 goto 的部分,詳細實作可以看 github
接著效能比較使用 perf 對 recursive 版本的 Fibonacci 來做比較:
/* computed goto */
$ perf stat --repeat 100 -e instructions,cycles ./as_exec --input 34 tests/fib-recursive.s
Performance counter stats for './as_exec --input 34 tests/fib-recursive.s' (100 runs):
186,0646,9616 instructions # 2.68 insns per cycle ( +- 0.00% )
69,3560,8922 cycles ( +- 0.07% )
19,9244,3216 branches ( +- 0.00% )
1862,3270 branch-misses # 0.93% of all branches ( +- 0.00% )
1.934058912 seconds time elapsed ( +- 0.08% )
/* switch */
$ perf stat --repeat 100 -e instructions,cycles ./as_exec --input 34 tests/fib-recursive.s
Performance counter stats for './as_exec --input 34 tests/fib-recursive.s' (100 runs):
190,7057,9491 instructions # 2.69 insns per cycle ( +- 0.00% )
70,9904,7359 cycles ( +- 0.25% )
23,0182,8702 branches ( +- 0.00% )
2256,1088 branch-misses # 0.98% of all branches ( +- 0.06% )
1.979487518 seconds time elapsed ( +- 0.25% )
這邊選擇觀察 instruction 跟 cycle 數量 以及 branch 跟 branch miss,在執行速度上差距比較不明顯,computed goto 大約比 switch 快 2%,而 branch 上 computed goto 都比 switch 少了 10% 以上 ( 其中 branches 減少 13%,branch miss 減少 17% )
一般的 recursion 會 call function 至最後一個 function return 之後再開始做計算,而 tail recursion 會在 call function 之前先做計算,拿 Fibonacci 做舉例
int fib_recursive(int n)
{
if (n == 0)
return 0;
if (n == 1)
return 1;
return fib_recursive(n - 1) + fib_recursive(n - 2);
}
int fib_tail_recursion(int n, int a, int b)
{
if (n == 0)
return a;
return fib_tail_recursion(n - 1, b, a + b);
}
recursive 版本的 Fibonacci 實作最後會回傳 fib(n - 1) + fib(n - 2),而這兩個 function 在最後也會同樣繼續呼叫 fib,直到 fib 的輸入值為 0 或 1 時才會開始回傳數值,並且將回傳的數值相加
tail recursion 在最後 call function 之前已經先做完相加了,即最後一個參數的位置 a + b
,相加完才傳給下一個 function,由於計算已經做完了,所以之後的 call function 就可以省去重複的計算
利用 perf 來比較兩者在 n = 10 的 cycles 跟 instructions:
Performance counter stats for './main' (10 runs):
59,3426 cycles ( +- 0.50% )
51,2857 instructions # 0.86 insns per cycle ( +- 0.33% )
0.001705757 seconds time elapsed ( +- 14.87% )
Performance counter stats for './main' (10 runs):
59,0573 cycles ( +- 0.40% )
50,8912 instructions # 0.86 insns per cycle ( +- 0.21% )
0.001778989 seconds time elapsed ( +- 18.14% )
上面為 tail recursion,下面為 recursive,可以注意到在 n = 10 時,兩者所花費的時間, cycles, instructions 相近,接著比較 n = 40 時的狀況:
Performance counter stats for './main' (10 runs):
59,3367 cycles ( +- 1.23% )
51,1270 instructions # 0.86 insns per cycle ( +- 0.68% )
0.001777684 seconds time elapsed ( +- 18.38% )
Performance counter stats for './main' (10 runs):
26,9610,2973 cycles ( +- 0.00% )
61,6681,1237 instructions # 2.29 insns per cycle ( +- 0.00% )
0.752938374 seconds time elapsed ( +- 0.10% )
上面為 tail recursion,下面為 recursive,可以發現到 n = 40 時,recursive 所花費的時間, cycles, instructions 有顯著的增加,這是由於過多的重複運算造成
透過迴圈從 f0 跟 f1 不斷相加直到要求的 index,程式碼如下:
int fib_iterative(int n)
{
int f0 = 0;
int f1 = 1;
int sum = n == 1 ? f1 : f0;
for (int i = 1; i < n; i++) {
sum = f0 + f1;
f0 = f1;
f1 = sum;
}
return sum;
}
透過不斷呼叫 fib(n - 1) + fib(n - 2)
的方式,讓程式一直往下呼叫,最後再將所有結果相加,程式碼如下:
int fib_recursive(int n)
{
if (n == 0)
return 0;
if (n == 1)
return 1;
return fib_recursive(n - 1) + fib_recursive(n - 2);
}
同樣是透過不斷往下呼叫 function,但是在呼叫前會先做計算,再把計算後的值當作參數傳入 function 中,程式碼如下,其中 a + b
就是預先計算的值:
int fib_tail_recursion(int n, int a, int b)
{
if (n == 0)
return a;
return fib_tail_recursion(n - 1, b, a + b);
}
參考 你所不知道的 C 語言 - 遞迴呼叫篇,Fibonacci 可透過矩陣轉換來計算:
到這邊為止,就可以透過 iterative 或 tail recursion 來計算
根據 Fibonacci Q-Matrix 的特性可以推導出以下公式:
透過這兩條公式,可以在時間複雜度為
以下為程式碼:
int fib_fast_doubling (int n)
{
if (n == 0) return 0;
if (n == 1) return 1;
int cbs = 30 - __builtin_clz(n);
int f1 = 1;
int f2 = 1;
int sum, sum_1;
while (cbs >= 0) {
sum = (f2 * 2 - f1) * f1;
sum_1 = f1 * f1 + f2 * f2;
f2 = sum_1;
f1 = sum;
if ((n >> cbs) & 1) {
f1 = f2;
f2 = sum_1 + sum;
}
cbs--;
}
return f1;
}
程式主要是從輸入值的最高有效位開始往前檢查每個 bit,如果 bit = 0 就計算 fib(2k) 及 fib(2k + 1),如果 bit = 1 除了計算 fib(2k), fib(2k + 1) 以外,再根據 fib(2k), fib(2k + 1) 計算 fib(2k + 2)
待補
需做 integer overflow 的檢查,必須將相加及相乘的運算用能夠處理 overflow 的 function 來取代
uint32_t add(uint32_t a, uint32_t b, int *carry)
{
*carry = (a >> 1) + (b >> 1) + (a & b & 1) >= (uint32_t)(1 << 31);
return a + b;
}
void mul(uint32_t a, uint32_t b, uint32_t *hi, uint32_t *lo)
{
uint32_t alo = a & 0xFFFF;
uint32_t blo = b & 0xFFFF;
uint32_t ahi = a >> 16;
uint32_t bhi = b >> 16;
int carry = 0;
*hi = (ahi * blo >> 16) + (alo * bhi >> 16);
*hi += ahi * bhi;
*lo = add((ahi * blo) & 0xFFFF << 16, (alo * bhi) & 0xFFFF << 16, &carry);
*hi += carry;
*lo = add(*lo, alo * blo, &carry);
*hi += carry;
}
用於在一個排序過的陣列中尋找 element,類似於 binary search,首先選擇一個 index 當作 entry,在 binary search 中為 array.length / 2
,fibonacci search 則為 n , where min(f(n)) >= array.length
接著如果沒找到就根據搜尋目標與現在索引的值的大小往左或往右移動 f(n - 1),若沒找到就繼續移動 f(n - 2) , f(n - 3) … 以此類推
與 binary search 比較:
由數學歸納法來證明:
故由數學歸納法原理得證
參考 github - simple.vm 的設計方式,利用 #if defined(profile)
將 profiler 相關的程式碼寫在其中,如蒐集 instruction, cycles 的數量等,如果 profile mode 就透過 gcc 的 -D 選項來定義 profile,normal mode 就不定義,因此在正常模式中就不會執行跟 profiler 相關的程式碼
相關程式碼放在 github
3 Dispatch Techniques
你所不知道的 C 語言 - 遞迴呼叫篇
github - simple.vm
fibonacci search