# 2018q3 Homework4 (assessment) contributed by < [`ofAlpaca`](https://github.com/ofAlpaca) > ###### tags: `CSIE5006` `HW` ### 測驗 `1` 考慮以下求絕對值的程式碼: ```C #include <stdint.h> int64_t abs64(int x) { if (x < 0) return -x; return x; } ``` 移除分支並善用[二補數](https://en.wikipedia.org/wiki/Two%27s_complement)特性,改寫為下方程式碼: ```C #include <stdint.h> int64_t abs64(int64_t x) { int64_t y = x A1 (A2 - 1); return (x A3 y) - y; } ``` 請補完,其中 `A1` 和 `A3` 都是 operator。 ==作答區== A1 = >> A2 = 64 A3 = ^ :::success 延伸問題: 1. 解釋運作原理,並探討可能的 overflow/underflow 議題; 2. 搭配下方 pseudo-random number generator (PRNG) 和考量到前述 (1),撰寫 `abs64` 的測試程式,並探討工程議題 (如:能否在有限時間內對 int64_t 數值範圍測試完畢?) ```C static uint64_t r = 0xdeadbeef int64_t rand64() { r ^= r >> 12; r ^= r << 25; r ^= r >> 27; return (int64_t) (r * 2685821657736338717); } ``` 3. 在 GitHub 找出類似用法的專案並探討,提示:密碼學相關 ::: ==想法與思考== * `int64_t` 是有號數,所以其 MSB 是 signed bit ,觀察一下待補的程式碼中的 `int64_t y = x A1 (A2 - 1)` 可以發現到需要對原本的數值 `x` 做運算,假設 `y` 是 signed bit ,這樣的話只要將 `x` 向右位移 63 bits 即可得到 signed bit, `x` 為正則為 0 , 為負則為 -1 。 ``` signed number shift : 1000 >> right shift 3 bits >> 1110 0000 >> right shift 3 bits >> 0000 ``` * 題目有提到善用**二補數的特性**,對有號數做二補數運算可以得到乘上 -1 的結果。透過絕對值的非負特性可以推出如果輸入數值為正數時,不做二補數運算,為負數時做二補數運算。 * 透過 `(x A3 y) - y` 可以得知,當 `x` 為負時, `y` 為 -1 ,若 `x` XOR `y` 即可得到 `x` 的一補數,再 -(-1) 即可得到 `x` 的二補數。當 `x` 為正,因為 `y` 為 0 故不影響。 ==延伸問題== 1. 解釋運作原理,並探討可能的 overflow/underflow 議題 * 運作原理如上述,此程式碼是存在 overflow 的問題, `int64_t` 的範圍是在 -(2^63^) 到 (2^63^-1) ,但如果 `x` 是 -(2^63^) 要求絕對值,那不可能表示得出 (2^63^) ,因為已經超出 64 bits 的表示範圍了。程式碼看來是不會有 underflow 的問題,因為當 `x` 是正數, `y` 會為 0 ,不可能會小於最小值。 ( -(2^63^) ~ (2^63-1) = -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807 ) * 以 4-bits abs 為例 : ``` 1000 = -8 ^ 1111 = -1 ------------ 0111 - 1111 = -1 ------------ 1000 = -8 // Overflow : abs(-8) should be 8 ``` 2. 搭配下方 pseudo-random number generator (PRNG) 和考量到前述 (1),撰寫 abs64 的測試程式,並探討工程議題 (如:能否在有限時間內對 int64_t 數值範圍測試完畢?) ```clike= static uint64_t r = 0xdeadbeef; int64_t abs64(int64_t x) { int64_t y = x >> (64 - 1); return (x ^ y) - y; } int64_t rand64() { r ^= r >> 12; r ^= r << 25; r ^= r >> 27; return (int64_t) (r * 2685821657736338717); } int main() { uint64_t t; time_t t1 = clock(); for(uint64_t i = 0; i < UINT_MAX ; i++){ if ((t = abs64(rand64())) < 0) { printf("Overflow with %ld\n", t); break; } time_t t2 = clock(); printf("Time : %lf sec\n", (t2 - t1)/(double)(CLOCKS_PER_SEC)); } ``` * 結果為 : ``` $ ./abs64_test Time : 26.715478 sec ``` * `rand64()` 所使用的為 [Xorshift](https://en.wikipedia.org/wiki/Xorshift) 的 PRNG ,週期相當長,為 2^64^ - 1 。 * `UINT_MAX` 為 32 bits 無號數的最大值,也就是 2^32^ = 4294967295 ,跑完花費了 26.7 秒。 * 將範圍改為兩倍的 `UINT_MAX` 發現花費時間變為 54 秒多,如果把範圍改為 64 bits ,也就是 2^64^ ,那麼應該會花費 26.7 乘上 2^32^ 的時間,大概是 114,675,626,803 秒,時間單位換算後為 3636 年,所以要在有限時間內測試完是不太可能的。 :::info 不太清楚為什麼要使用 PRNG 來測試程式,是因為若把 `int64_t` 的範圍都測試過太耗費時間,所以改採用 PRNG 來隨機抽樣測試程式? ::: --- ### 測驗 `2` 考慮測試 C 編譯器 [Tail Call Optimization](https://en.wikipedia.org/wiki/Tail_call) (TCO) 能力的程式 [tco-test](https://github.com/sysprog21/tco-test),在 gcc-8.2.0 中抑制最佳化 (也就是 `-O0` 編譯選項) 進行編譯,得到以下執行結果: ```shell $ gcc -Wall -Wextra -Wno-unused-parameter -O0 main.c first.c second.c -o chaining $ ./chaining No arguments: no TCO One argument: no TCO Additional int argument: no TCO Dropped int argument: no TCO char return to int: no TCO int return to char: no TCO int return to void: no TCO ``` 而在開啟最佳化 (這裡用 `-O2` 等級) 編譯,會得到以下執行結果: ```shell $ gcc -Wall -Wextra -Wno-unused-parameter -O2 main.c first.c second.c -o chaining $ ./chaining No arguments: TCO One argument: TCO Additional int argument: TCO Dropped int argument: TCO char return to int: no TCO int return to char: no TCO int return to void: TCO ``` 注意 [__builtin_return_address](https://gcc.gnu.org/onlinedocs/gcc/Return-Address.html) 是 gcc 的內建函式: > This function returns the return address of the current function, or of one of its callers. The level argument is number of frames to scan up the call stack. A value of 0 yields the return address of the current function, a value of 1 yields the return address of the caller of the current function, and so forth. When inlining the expected behavior is that the function returns the address of the function that is returned to. To work around this behavior use the noinline function attribute. > The level argument must be a constant integer. 從實驗中可發現下方程式無法對 `g` 函式施加 TCO: ```C void g(int *p); void f(void) { int x = 3; g(&x); } void g(int *p) { printf("%d\n", *p); } ``` 因為函式 `f` 的區域變數 `x` 在返回後就不再存在於 stack。考慮以下程式碼: ```C= int *global_var; void f(void) { int x = 3; global_var = &x; ... /* Can the compiler perform TCO here? */ g(); } ``` 思考程式註解,在第 8 行能否施加 TCO 呢?選出最適合的解釋。 ==作答區== 只要函式 `g` 沒有對 `global_var` 指標作 dereference,那麼 TCO 就有機會 :::success 延伸問題: 1. 探討 TCO 和遞迴程式的原理 2. 分析上述實驗的行為和解釋 gcc 對 TCO 的操作 3. 在 [Android 原始程式碼](https://android.googlesource.com/) 裡頭找出 [__builtin_return_address](https://gcc.gnu.org/onlinedocs/gcc/Return-Address.html) 的應用並解說 ::: ==想法與思考== 若是可使用 TCO ,當 `f` 函式尾呼叫 `g` 函式後,編譯器為了能將目前 `f` 的 stack frame 給 `g` 函式使用,會修改其內的變數,變數 `x` 就此消失在 stack 中,反正之後 `g` 函式也用不著,所以不影響。但如果 `g` 函式中用到了 `global_var` 的 dereference ,為了保存 `x` 的值,一定要保留其 stack frame ,因此就無法進行 TCO 。 ==延伸問題== 1. 探討 TCO 和遞迴程式的原理 * Tail Call Optimization 又被稱為 Tail Call Elimination ,那是因為 TCO 會保留目前的 stack frame 來給尾呼叫的函式 ( callee ) 使用,為了能給該函式使用,所以需要**修改或消除**其內的變數,雖然這樣會遺失原本 stack frame 的資料,但是因為尾呼叫的函式已經是在呼叫的函式 ( caller ) 的最後,該做的都已經做完了,所以其內的資料也已經不再重要了。 ![](https://i.imgur.com/XOP125l.png) * 遞迴程式在呼叫自己時,會產生新的 stack frame ,但當呼叫大量遞迴時,就會造成 stack overflow ,為了防止這類事情發生,可以使用上述 TCO 的方法來減少 stack frame 的產生次數,此稱為 Tail call Recursion 。 2. 分析上述實驗的行為和解釋 gcc 對 TCO 的操作 * 根據以上實驗改為以下程式碼 : ```clike= #include <stdio.h> int *global_var; void *f_ra; void *g_ra; void g() { int y = 5; g_ra = __builtin_return_address(0); }; void f(void) { int x = 3; global_var = &x; f_ra = __builtin_return_address(0); g(); } int main() { f(); printf(f_ra == g_ra ? "TCO\n" : "no TCO\n"); } ``` * 不使用 gcc 最佳化 `$ gcc test.c -o test -O0` 後 `$ objdump -d test` 觀察其 `f()` 與 `g()` 的組合語言 : * 從 25 行的 `callq` 可以得知沒最佳化的程式需要 call stack 來呼叫函式 * 在 `g()` 的 scope 對 `g_ra` 作 assignment * 最後在 pop `$rbp` 回去呼叫他的函式 * 在 `f()` 的 scope 對 `f_ra` 作 assignment ```= 00000000000006aa <g>: 6aa: 55 push %rbp 6ab: 48 89 e5 mov %rsp,%rbp 6ae: c7 45 fc 05 00 00 00 movl $0x5,-0x4(%rbp) 6b5: 48 8b 45 08 mov 0x8(%rbp),%rax 6b9: 48 89 05 58 09 20 00 mov %rax,0x200958(%rip) # 201018 <g_ra> 6c0: 90 nop 6c1: 5d pop %rbp 6c2: c3 retq 00000000000006c3 <f>: 6c3: 55 push %rbp 6c4: 48 89 e5 mov %rsp,%rbp 6c7: 48 83 ec 10 sub $0x10,%rsp 6cb: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax 6d2: 00 00 6d4: 48 89 45 f8 mov %rax,-0x8(%rbp) 6d8: 31 c0 xor %eax,%eax 6da: c7 45 f4 03 00 00 00 movl $0x3,-0xc(%rbp) 6e1: 48 8d 45 f4 lea -0xc(%rbp),%rax 6e5: 48 89 05 34 09 20 00 mov %rax,0x200934(%rip) # 201020 <global_var> 6ec: 48 8b 45 08 mov 0x8(%rbp),%rax 6f0: 48 89 05 31 09 20 00 mov %rax,0x200931(%rip) # 201028 <f_ra> 6f7: b8 00 00 00 00 mov $0x0,%eax 6fc: e8 a9 ff ff ff callq 6aa <g> 701: 90 nop 702: 48 8b 45 f8 mov -0x8(%rbp),%rax 706: 64 48 33 04 25 28 00 xor %fs:0x28,%rax 70d: 00 00 70f: 74 05 je 716 <f+0x53> 711: e8 5a fe ff ff callq 570 <__stack_chk_fail@plt> 716: c9 leaveq 717: c3 retq ``` * 另一邊使用 gcc 最佳化 `$ gcc test.c -o test -O2` 後 `$ objdump -d test` 觀察其 `f()` 與 `g()` 的組合語言 : * 最佳化後的程式沒有使用到 `callq` * 第 18 、 19 行對 `f_ra` 與 `g_ra` 的 assignment 也都是在 `f()` 的 scope 下完成 * 所以最後的 `f_ra` 會等於 `g_ra` :::info 不太清楚為什麼 `g_ra` 會被 assign 兩次? ::: ```= 0000000000000750 <g>: 750: 48 8b 04 24 mov (%rsp),%rax 754: 48 89 05 bd 08 20 00 mov %rax,0x2008bd(%rip) # 201018 <g_ra> 75b: c3 retq 75c: 0f 1f 40 00 nopl 0x0(%rax) 0000000000000760 <f>: 760: 48 83 ec 18 sub $0x18,%rsp 764: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax 76b: 00 00 76d: 48 89 44 24 08 mov %rax,0x8(%rsp) 772: 31 c0 xor %eax,%eax 774: 48 8d 44 24 04 lea 0x4(%rsp),%rax 779: c7 44 24 04 03 00 00 movl $0x3,0x4(%rsp) 780: 00 781: 48 89 05 98 08 20 00 mov %rax,0x200898(%rip) # 201020 <global_var> 788: 48 8b 44 24 18 mov 0x18(%rsp),%rax 78d: 48 89 05 94 08 20 00 mov %rax,0x200894(%rip) # 201028 <f_ra> 794: 48 89 05 7d 08 20 00 mov %rax,0x20087d(%rip) # 201018 <g_ra> 79b: 48 8b 44 24 08 mov 0x8(%rsp),%rax 7a0: 64 48 33 04 25 28 00 xor %fs:0x28,%rax 7a7: 00 00 7a9: 75 05 jne 7b0 <f+0x50> 7ab: 48 83 c4 18 add $0x18,%rsp 7af: c3 retq 7b0: e8 eb fd ff ff callq 5a0 <__stack_chk_fail@plt> 7b5: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 7bc: 00 00 00 7bf: 90 nop ``` 參考 : [Tail Call Optimization](http://2ality.com/2015/06/tail-call-optimization.html) [Wiki](https://en.wikipedia.org/wiki/Tail_call) 3. 在 [Android 原始程式碼](https://android.googlesource.com/) 裡頭找出 [__builtin_return_address](https://gcc.gnu.org/onlinedocs/gcc/Return-Address.html) 的應用並解說 在 [/kernel/common/android-3.10/./include/linux/timer.h](https://android.googlesource.com/kernel/common/+/android-3.10/include/linux/timer.h/#215) 找到 `__builtin_return_address` 的使用 : ```clie= static inline void timer_stats_timer_set_start_info(struct timer_list *timer) { if (likely(!timer_stats_active)) return; __timer_stats_timer_set_start_info(timer, __builtin_return_address(0)); } ``` 在 [timer.c](https://elixir.bootlin.com/linux/v4.9/source/kernel/time/timer.c#L575) 中的 `__timer_stats_timer_set_start_info` ```clike= void __timer_stats_timer_set_start_info(struct timer_list *timer, void *addr) { if (timer->start_site) return; timer->start_site = addr; memcpy(timer->start_comm, current->comm, TASK_COMM_LEN); timer->start_pid = current->pid; } ``` `timer` 是 linux kernel 中的計時器,可以將指定工作延後到特定時間執行,類似工作排程。 `timer_stats_timer_set_start_info` 函式負責紀錄呼叫計時器的函式的位址與其資訊,儲存至 `struct timer_list` 的資料結構。 參考: [struct timer_list](https://blog.csdn.net/David_xtd/article/details/38979347) [Kernel Timers](https://www.oreilly.com/library/view/linux-device-drivers/0596000081/ch06s05.html) --- ### 測驗 `3` 以下程式碼編譯並執行後,在 x86_64 GNU/Linux 會遇到記憶體存取錯誤: ```shell $ cat ptr.c int main() { int *ptr = 0; return *ptr; } $ gcc -o ptr ptr.c $ ./ptr Segmentation fault: 11 ``` 分別考慮以下 4 個程式,探討其行為。 - [ ] `ptr1.c` ```C int main() { return *((int *) 0); } ``` - [ ] `ptr2.c` ```C int main() { return &*((int *) 0); } ``` - [ ] `ptr3.c` ```C #include <stddef.h> int main() { return &*NULL; } ``` - [ ] `ptr4.c` ```C #include <stddef.h> int main() { return &*(*main - (ptrdiff_t) **main); } ``` ==作答區== K1 = `ptr1.c` 在執行時期會造成 Segmentation fault K2 = `ptr2.c` 是合法 C 程式,在執行後可透過 `echo $?` 得到 exit code 為 `0` K3 = `ptr3.c` 是合法 C 程式,在執行後可透過 `echo $?` 得到 exit code 為 `0` K4 = `(c)` `ptr4.c` 是合法 C 程式,在執行後可透過 `echo $?` 得到 exit code 為 `0` :::success 延伸問題: 1. 參照 C 語言規格書,充分解釋其原理 2. 解析 clang/gcc 編譯器針對上述程式碼的警告訊息 3. 思考 `Segmentation fault` 的訊息是如何顯示出來,請以 GNU/Linux 為例解說。提示: Page fault handler ::: ==想法與思考== * `ptr1.c` : 先將 0 轉型為 pointer to int 後再 dereference 。由於指標指向的位址為 0 ,在 dereference 時無法定址故會產生 Segementation fault 。 ```C int main() { return *((int *) 0); } ``` * `ptr2.c` : 先將 0 轉型為 pointer to int 後 dereference 再 address of 。 `&*` 抵銷,相當於只回傳了 pointer to int 這個指標,故為 0 。 ```C int main() { return &*((int *) 0); } ``` * `ptr3.c` : `NULL` 是個 macro ,可以看成是 `((char *)0)` ,接下來的原理就和 `ptr2.c` 一樣了。 ```C int main() { return &*NULL; } ``` * `ptr4.c` : * `main` 是個 function designator ,只要是 function designator 都會被自動轉為 pointer to function , `*main` 則是對 `main` 作 dereference ,將其轉回 function designator ,但由於上述原因,還是會被轉回 pointer to function , `**main` 也一樣。 * `ptrdiff_t` 是紀錄兩指標相減的結果。 * `*main` 與 `**main` 兩指標是指向同個位址,相減為 0 ,又因 `&*` 兩者會抵銷,故回傳 0 。 ```C int main() { return &*(*main - (ptrdiff_t) **main); } ``` ==延伸問題== 1. 參照 C 語言規格書,充分解釋其原理 * 根據 C99 可以得知,如果 dereference 非法位址的指標,會是為定義行為。 > C99 [6.5.3.2] If an invalid value has been assigned to the pointer, the behavior of the unary * operator is undefined. * 根據 C99 可以得知,如果在 `&*` 的情況下會忽略這兩個運算子。 > C99 [6.5.3.2] If the operand is the result of a unary * operator, neither that operator nor the & operator is evaluated and the result is as if both were omitted, except that the constraints on the operators still apply and the result is not an lvalue. * C99 當中有提到 `NULL` 是個定義於 `stddef.h` 的 macro 。 > C99 [7.17] NULL which expands to an implementation-defined null pointer constant; * 根據 [glibc](http://www.gnu.org/software/libc/manual/html_node/Null-Pointer-Constant.html) `NULL` 通常被定義為 `(void*) 0` 或 `0` 。 * Function designator 會被轉為 pointer to function 。 > C99 [6.3.2.1]A function designator is an expression that has function type. Except when it is the operand of the sizeof operator or the unary & operator, a function designator with type ‘function returning type ’’ is converted to an expression that has ype ‘‘pointer to function returning type ’’. * 根據 C99 可以得知 `ptrdiff_t` 是兩指標相減後的結果,型別為 signed integer 。 > C99 [7.17] ptrdiff_t which is the signed integer type of the result of subtracting two pointers; 2. 解析 clang/gcc 編譯器針對上述程式碼的警告訊息 * `ptr2.c` 使用 gcc 編譯時會產生以下警告 : * 由於 main 的回傳型別是 int ,但 return 的卻是 pointer to int ,故會產生警告 * 警告是在說明程式想要用 pointer to int 當 int 回傳但卻沒有作 type casting * 如果將 `int main` 改為 `int * main` 就不會有警告 ``` t4.c: In function ‘main’: t4.c:4:21: warning: return makes integer from pointer without a cast [-Wint-conversion] int main() { return &*((int *) 0);} ^~~~~~~~~~~~~ ``` * `ptr3.c` 使用 gcc 編譯時會產生以下警告 : * gcc 不知道 `void *` 大小,所以無法 dereference * 那為什麼還可以跑?根據 C99 的 footnote 提到的, `&*` 可以抵銷,就算是 null pointer > C99 [6.5.3.2 footnote(84)] Thus, &*E is equivalent to E (even if E is a null pointer) ``` t4.c: In function ‘main’: t4.c:4:22: warning: dereferencing ‘void *’ pointer int main() { return &*NULL; } ^ ``` * `ptr4.c` 使用 gcc 編譯時會產生以下警告 : * 原因和 `ptr2.c` 一樣 ``` t4.c: In function ‘main’: t4.c:4:20: warning: return makes integer from pointer without a cast [-Wint-conversion] int main() {return &*(*main - (ptrdiff_t) **main);} ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``` 3. 思考 `Segmentation fault` 的訊息是如何顯示出來,請以 GNU/Linux 為例解說。提示: Page fault handler * 當程式存取到非法的記憶體位址時, CPU 會產生 page fault exception 並呼叫 page fault handler。 * page fault handler 會根據 page fault 的情況來決定如何處理,如果 Page fault 發生的位址不存在於 Virtual memory address ,那他就是非法的, kernel 會送出 `SIGSEGV` 的 signal 訊號。 * 如果程式當中有 signal handler ,則會交由其處理,如果沒有則會交給 kernel 的 default signal handler 。 * 根據 [signal](http://man7.org/linux/man-pages/man7/signal.7.html) 的文件表示,預設的 `SIGSEGV` handler 是 [Core](http://man7.org/linux/man-pages/man5/core.5.html) ,他會負責中止程式並產生 core dump file , `Segmentation fault` 也是在此印出。 * Core dump file 可用於 gdb 來 debug 。 * 以下程式為自訂義 signal handler ,之後再呼叫 default signal handler (`SIG_DFL`) : ```clike void sighandler(int signum) { printf("Process %d got signal %d\n", getpid(), signum); signal(signum, SIG_DFL); kill(getpid(), signum); } int main() { signal(SIGSEGV, sighandler); char *str = "Hello"; str[1] = 'h'; return 0; } ``` * 從輸出可以發現印出了熟悉的 `Segmentation fault` ,由此可知此訊息是由 `SIG_DFL` 這個 default signal handler 產生。 ``` $ ./sgf.out Process 6331 waits for someone to send it SIGSEGV Segmentation fault (core dumped) ``` 參考 : [Page fault vs Segmentation fault](https://stackoverflow.com/questions/6950549/segmentation-fault-vs-page-fault) [Linux kernel 內存訪問與業缺失](https://feilengcui008.github.io/post/linux%E5%86%85%E6%A0%B8%E5%86%85%E5%AD%98%E8%AE%BF%E9%97%AE%E4%B8%8E%E7%BC%BA%E9%A1%B5%E4%B8%AD%E6%96%AD/) [Exception handling](https://www.kernel.org/doc/Documentation/x86/exception-tables.txt) [How to handle SIGSEGV, but also generate a core dump](http://www.alexonlinux.com/how-to-handle-sigsegv-but-also-generate-core-dump) [How to find default signal handler](https://stackoverflow.com/questions/40508007/how-to-find-the-source-code-of-the-default-signal-handler-function) --- ### 閱讀啟發與感想 沒有付出沒有收穫,這道理大家都知道,但是當真的要付出時又會願意付出多少,這篇 [因為自動飲料機而延畢的那一年](http://opass.logdown.com/posts/1273243-the-story-of-auto-beverage-machine-1) 裡的學長付出了大量的時間與努力才有那樣的自動飲料機,過程不只艱辛還很燒錢,這讓我不禁開始思考,到底是什麼支撐著他,是想要大賺錢的慾望?或是純粹的熱情?這個答案可能只有他本人知道,不過有一點可以確定的是,他一定抱持著就算付出慘痛代價也要成功的「決心」。想要有收穫就勢必要付出代價,但那些代價往往比自己所想的還來得沉重。 修完 4 週的課程後,首先發覺的是「自己的程度遠比自己所想的還差」,原本認為看完直播後寫作業可能需花 4 小時,但實際時數出來往往是 5 個小時以上,自己閱讀與理解的速度跟不上老師的預期,這其實讓我相當挫折,再加上實驗室的計畫、報論文更讓我兩頭燒,曾經想過放棄~~圖形理論與行動網路~~,來讓自己好過點,但最後總會想到自己來讀碩士的初衷,我不希望離開成大時只帶走一張畢業證書,而是成為一個看得起自己的人。