-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.json
More file actions
1 lines (1 loc) · 26.3 KB
/
Copy pathcontent.json
File metadata and controls
1 lines (1 loc) · 26.3 KB
1
{"posts":[{"title":"mmap and sigbus(上)","text":"同樣的code,在不同案子就是會有不同的驚喜(心累)。這次是syslog-ng會很隨機的crash。 12Program terminated with signal SIGBUS, Bus error. #0 0x0000007f89a378a4 in ?? () from /lib/ld-musl-aarch64.so.1 再找到ld-musl-aarch64.so.1映射的範圍 1234(gdb) info proc mappings 0x7f899de000 0x7f89a4e000 0x70000 0x0 /lib/libc.so0x7f89a378a4 - 0x7f899de000 = 0x598a4 知道crash的位置在libc.so offset 0x598a4。 objdump libc.so 120000000000059814 <do_tzset>: 598a4: 39400000 ldrb w0, [x0] 出示的指令是ldrb,用來讀取記憶體並放入register。 重新編譯一個帶debug symbol的版本,offset有跑掉,只能找一個大概的位置 12345678910111213141516171819202122 s = tzfile = (void *)__map_file("/etc/TZ", &tzfile_size); 5b158: 91192281 add x1, x20, #0x648 5b15c: d00000a0 adrp x0, 71000 <reserved.5595+0x6d0> 5b160: 910f2c00 add x0, x0, #0x3cb 5b164: 97fffe34 bl 5aa34 <__map_file> 5b168: f9033660 str x0, [x19,#1640] 5b16c: f90037a0 str x0, [x29,#104] } if (!s) s = "/etc/localtime"; 5b170: f94037a0 ldr x0, [x29,#104] 5b174: b5000080 cbnz x0, 5b184 <do_tzset+0x94> 5b178: d00000a0 adrp x0, 71000 <reserved.5595+0x6d0> 5b17c: 910f4c00 add x0, x0, #0x3d3 5b180: f90037a0 str x0, [x29,#104] if (!*s) s = __gmt; 5b184: f94037a0 ldr x0, [x29,#104] 5b188: 39400000 ldrb w0, [x0] 5b18c: 35000080 cbnz w0, 5b19c <do_tzset+0xac> 5b190: b00000a0 adrp x0, 70000 <protos+0xa0> 5b194: 9122e000 add x0, x0, #0x8b8 5b198: f90037a0 str x0, [x29,#104] 這幾行code中跟ldrb有關的就是dereference,而 s 指向file backed memory。 搜尋一下還有誰會去存取/etc/TZ,結果發現有另一個程序會使用fprintf去寫入該檔案。寫一個測試檔案跑跑看 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485#define SHARE_FILE "./test" static char* get_rand_str() { const int max = 100; const int min = 1; int len = rand() % max + min; char* ret = calloc(1, len); getrandom(ret, len, 0); return ret; } static void write_f(void) { FILE *fp = fopen(SHARE_FILE, "w"); char* rand_str = get_rand_str(); fprintf(fp, "%s\\n", rand_str); fclose(fp); free(rand_str); } static void read_f(void) { int fd = open(SHARE_FILE, O_RDONLY | O_NONBLOCK); struct stat statbuf; int err = fstat(fd, &statbuf); printf("stat size: %ld\\n", statbuf.st_size); char *ptr = mmap(NULL, statbuf.st_size, PROT_READ, MAP_SHARED, fd, 0); close(fd); if (*ptr) printf("."); munmap(ptr, statbuf.st_size); } static void* mmap_runner() { int i = 0; while(i < 100) { read_f(); ++i; usleep(500); } pthread_exit(NULL); } static void* fio_runner() { int i = 0; while(i < 100) { write_f(); ++i; usleep(500); } pthread_exit(NULL); } int main() { pthread_t mmap_thd; pthread_t fio_thd; pthread_create(&mmap_thd, NULL, &mmap_runner, NULL); pthread_create(&fio_thd, NULL, &fio_runner, NULL); pthread_join(mmap_thd, NULL); pthread_join(fio_thd, NULL); return 0; } 兩個thread,一個thread使用fopen/fprintf去寫檔,一個使用mmap讀檔,跑個幾次後就能觸發sigbus 1234Thread 2 "a.out" received signal SIGBUS, Bus error. [Switching to Thread 0x7ffff77c4700 (LWP 22134)] 0x0000555555554d94 in read_f () at test_mmap.c:68 68 if (*ptr) 與專案一樣掛在dereference地方。","link":"/2024/02/09/mmap%20and%20sigbus_1/"},{"title":"mmap and sigbus(下)","text":"在上一篇追蹤引發sigbus的程式碼片段,這篇來寫寫如何解決 回顧一下musl出問題的片段 123456789101112s = getenv("TZ");/* if TZ is empty try to read it from /etc/TZ */if (!s || !*s) { if (tzfile) __munmap((void*)tzfile, tzfile_size); s = tzfile = (void *)__map_file("/etc/TZ", &tzfile_size);}if (!s) s = "/etc/localtime";if (!*s) s = __gmt; 因此如果從環境變數 TZ 能取得值,則不會將 /etc/TZ map 進 memory,因此在帶起程式之前先給予TZ值 1TZ="GMT" program 或者在c code裡使用setenv設定環境變數 1setenv("TZ", "GMT", 1); 都能避免產生race condition。此外沒有給予環境變數時觀察/proc/self/maps可以發現TZ的存在 12$ cat /proc/152/maps7f881d9000-7f881da000 r--s 00000000 00:0f 6623 /etc/TZ 指定TZ就不會看到他出現了,解決。 番外在網上搜尋時發現,這個使用mmap讀取/etc/TZ的code並不是原生MUSL的作法,而是OpenWrt自己的Patch /toolchain/musl/patches/110-read_timezone_from_fs.patch 且該Patch從9年前就存在,出現在各版本的OpenWrt內,也有人提出可能導致sigbus的警告 https://github.com/openwrt/openwrt/issues/8758","link":"/2024/02/17/mmap%20and%20sigbus_2/"},{"title":"__init 與 module_init","text":"在工作的時候常看到這兩個keyword,不禁冒出兩個問題 module_init和__init的差別是什麼? 如果只有module_init,沒有__init會發生什麼事? What is __init__init比較單純是個巨集 123#define __init __section(".init.text") __cold __latent_entropy __noinitretpoline#define __initdata __section(".init.data")#define __initconst __section(".init.rodata") __section用於指定symbol存放的section,上面例子指定的section都是.init。該section內的指令會在entry point前執行,結束後就被釋放。而.text存放指令,.data和.rodata存放變數。 What is module_init看module_init前,要先知道module有分靜態及動態載入,靜態直接編譯進kernel,動態則透過insmod載入。編譯時obj-y為靜態載入,obj-m為動態載入。module_init因應這兩種方式有不同的實作。 static moduleMODULE是編譯時由GCC定義,沒定義MODULE時代表為靜態載入。 12345678#ifndef MODULE#define module_init(x) __initcall(x);#else#define module_init(initfn) \\ static inline initcall_t __maybe_unused __inittest(void) \\ { return initfn; } \\ int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));#endif 追一下__initcall,會發現__initcall等於device_initcall等於__define_initcall(fn, 6)。 12345678910111213141516171819202122232425#define __define_initcall(fn, id) \\ static initcall_t __initcall_##fn##id __used \\ __attribute__((__section__(".initcall" #id ".init"))) = fn;#define early_initcall(fn) __define_initcall(fn, early)#define pure_initcall(fn) __define_initcall(fn, 0)#define core_initcall(fn) __define_initcall(fn, 1)#define core_initcall_sync(fn) __define_initcall(fn, 1s)#define postcore_initcall(fn) __define_initcall(fn, 2)#define postcore_initcall_sync(fn) __define_initcall(fn, 2s)#define arch_initcall(fn) __define_initcall(fn, 3)#define arch_initcall_sync(fn) __define_initcall(fn, 3s)#define subsys_initcall(fn) __define_initcall(fn, 4)#define subsys_initcall_sync(fn) __define_initcall(fn, 4s)#define fs_initcall(fn) __define_initcall(fn, 5)#define fs_initcall_sync(fn) __define_initcall(fn, 5s)#define rootfs_initcall(fn) __define_initcall(fn, rootfs)#define device_initcall(fn) __define_initcall(fn, 6)#define device_initcall_sync(fn) __define_initcall(fn, 6s)#define late_initcall(fn) __define_initcall(fn, 7)#define late_initcall_sync(fn) __define_initcall(fn, 7s)#define __initcall(fn) device_initcall(fn) 考慮__module_init(hello_init)展開後 1static initcall_t __initcall_hello_init6 __used __attribute__((__section__(".initcall6.init"))) = hello_init; 現在需要拆開分析 首先initcall_t是一個無參數,回傳值為int的function pointer。 1typedef int (*initcall_t)(void); 接著__used避免gcc拋出variable defined but not used警告。 1#define __used __attribute__((__used__)) 然後__attribute__((__section__(".initcall6.init")))告訴linker將此symbol放到section .initcall6.init。 整個結合起來看module_init做的事情是: 宣告一個叫做__initcall_hello6的function pointer,其prototype為typedef int (*initcall_t)(void),指標指向hello_init,最後將__initcall_hello_init6放到section .initcall6.init。 接著看/arch/arm64/kernel/vmlinux.lds.S,這是linux的linker script。 12345678.init.data : { INIT_DATA INIT_SETUP(16) INIT_CALLS CON_INITCALL INIT_RAM_FS *(.init.altinstructions .init.bss) /* from the EFI stub */} 這裡用的巨集非常多,先專注在INIT_CALLS就好,定義在/include/asm-generic/vmlinux.lds.h 123456789101112131415161718#define INIT_CALLS_LEVEL(level) \\ __initcall##level##_start = .; \\ KEEP(*(.initcall##level##.init)) \\ KEEP(*(.initcall##level##s.init)) \\#define INIT_CALLS \\ __initcall_start = .; \\ KEEP(*(.initcallearly.init)) \\ INIT_CALLS_LEVEL(0) \\ INIT_CALLS_LEVEL(1) \\ INIT_CALLS_LEVEL(2) \\ INIT_CALLS_LEVEL(3) \\ INIT_CALLS_LEVEL(4) \\ INIT_CALLS_LEVEL(5) \\ INIT_CALLS_LEVEL(rootfs) \\ INIT_CALLS_LEVEL(6) \\ INIT_CALLS_LEVEL(7) \\ __initcall_end = .; 展開後,要懂一點ld script才看得懂 1234567891011.init.data { __initcall_start = .; *(.initcallearly.init) __initcall0_start = .; *(.initcall0.init) *(.initcall0s.init) __initcall1_start = .; *(.initcall1.init) *(.initcall1s.init) __initcall_end = .;} 這裡定義了一個section .init.data,大括號內為其內容。 1234__initcall_start = .;__initcall0_start = .;__initcall1_start = .;__initcall_end = .; 這些symbol標記當前起始的記憶體位置。 重點是*(.initcall0.init),*是一個wildcard,代表所有object file,.initcall0.init是section name,組合起來就是所有object file的.initcall0.init都放進.init.data。 然後回來看/init/main.c,initcall_entry_t 等於 initcall_t。前面的__initcall_start = .;可以當作變數使用。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051extern initcall_entry_t __initcall_start[];extern initcall_entry_t __initcall0_start[];extern initcall_entry_t __initcall1_start[];extern initcall_entry_t __initcall2_start[];extern initcall_entry_t __initcall3_start[];extern initcall_entry_t __initcall4_start[];extern initcall_entry_t __initcall5_start[];extern initcall_entry_t __initcall6_start[];extern initcall_entry_t __initcall7_start[];extern initcall_entry_t __initcall_end[];static initcall_entry_t *initcall_levels[] __initdata = { __initcall0_start, __initcall1_start, __initcall2_start, __initcall3_start, __initcall4_start, __initcall5_start, __initcall6_start, __initcall7_start, __initcall_end,};static void __init do_initcalls(void){ for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++) { do_initcall_level(level, command_line); }}static void __init do_initcall_level(int level, char *command_line){ initcall_entry_t *fn; for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++) do_one_initcall(initcall_from_entry(fn));}int __init_or_module do_one_initcall(initcall_t fn){ // 可以用在cmdline指定禁用的kernel module if (initcall_blacklisted(fn)) return -EPERM; // tracepoint do_trace_initcall_start(fn); ret = fn(); do_trace_initcall_finish(fn, ret); return ret;} call path: 12345678910head.S: bl start_kernel start_kernel arch_call_rest_init rest_init kernel_init kernel_init_freeable do_basic_setup do_initcalls do_initcall_level do_one_initcall dynamic module1234#define module_init(initfn) \\ static inline initcall_t __maybe_unused __inittest(void) \\ { return initfn; } \\ int init_module(void) __copy(initfn) __attribute__((alias(#initfn))); __inittest用來檢查我們傳入的function是否符合initcall_t的prototype,否則會由compiler拋出錯誤。 123/home/zanets/projects/kernel_module/hello-1.c: In function ‘__inittest’:/home/zanets/projects/kernel_module/hello-1.c:20:13: error: returning ‘unsigned int (*)(void’ from a function with incompatible return type ‘initcall_t’ {aka ‘int (*)(void)’} [-Werror=incompatible-pointer-types] 接著宣告 function init_module並使用了__copy和alias(#initfn)。 123# define __copy(symbol) __attribute__((__copy__(symbol)))#define __alias(symbol) __attribute__((__alias__(#symbol))) copy常與alias共同使用,用來複製__attribute__到alias function。參考GCC documenthttps://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html 所以在動態載入時,module_init會將指定的init function取一個別名叫做init_module。而insmod會呼叫syscall init_module,以下是init_module的call flow 12345copy_module_from_user(umod, len, &info);load_module (呼叫copy_module_elf,可以看到它是怎麼讀取ELF) > setup_load_info > do_init_module > do_one_initcall(mod->init) 在 do_init_module會free init section 12345freeinit->init_text = mod->mem[MOD_INIT_TEXT].base;freeinit->init_data = mod->mem[MOD_INIT_DATA].base;freeinit->init_rodata = mod->mem[MOD_INIT_RODATA].base;...kfree(freeinit); 但我一直找不到mod->init被assign的點,他就這樣被呼叫了,Magic!還好有人有一樣的問題https://stackoverflow.com/questions/68152475/when-does-mod-init-be-assigned編譯external kernel module會多出一個kmod.mod.c,這是由modpost產生的 123456789__visible struct module __this_module__section(".gnu.linkonce.this_module") = { .name = KBUILD_MODNAME, .init = init_module,#ifdef CONFIG_MODULE_UNLOAD .exit = cleanup_module,#endif .arch = MODULE_ARCH_INIT,}; 1234Relocation section '.rela.gnu.linkonce.this_module' at offset 0x13588 contains 2 entries: Offset Info Type Sym. Value Sym. Name + Addend000000000138 003000000101 R_AARCH64_ABS64 0000000000000000 init_module + 0000000000330 002f00000101 R_AARCH64_ABS64 0000000000000000 cleanup_module + 0 在 setup_load_info 裡 1info->index.mod = find_sec(info, ".gnu.linkonce.this_module"); struct load_info* info 在之後由 layout_and_allocate 函數轉換成 struct module* mod,最後就由 do_one_initcall 執行我們指定的init function。 結論所以,module_init和__init的差別是什麼?__init用於將init function放到.init.text,而module_init用於將init function掛到kernel init list裡,等待kernel執行。 那如果只有module_init,沒有__init會發生什麼事?__init會將init function放到 .init.text section,init階段結束後會把 .init.text 移除。若沒有__init標記則會放到.text,init function沒有從section中移除,導致記憶體浪費。 initcall_blacklisted在initcall_blacklisted裡的initcall不會被執行,也許可以用來debug。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647__setup("initcall_blacklist=", initcall_blacklist);static __initdata_or_module LIST_HEAD(blacklisted_initcalls);static int __init initcall_blacklist(char *str){ char *str_entry; struct blacklist_entry *entry; /* str argument is a comma-separated list of functions */ do { str_entry = strsep(&str, ","); if (str_entry) { entry = memblock_alloc(sizeof(*entry), SMP_CACHE_BYTES); strcpy(entry->buf, str_entry); list_add(&entry->next, &blacklisted_initcalls); } } while (str_entry); return 1;}static bool __init_or_module initcall_blacklisted(initcall_t fn){ struct blacklist_entry *entry; char fn_name[KSYM_SYMBOL_LEN]; unsigned long addr; if (list_empty(&blacklisted_initcalls)) return false; addr = (unsigned long) dereference_function_descriptor(fn); sprint_symbol_no_offset(fn_name, addr); /* * fn will be "function_name [module_name]" where [module_name] is not * displayed for built-in init functions. Strip off the [module_name]. */ strreplace(fn_name, ' ', '\\0'); list_for_each_entry(entry, &blacklisted_initcalls, next) { if (!strcmp(fn_name, entry->buf)) { return true; } } return false;} __setup("initcall_blacklist=", initcall_blacklist);代表在cmdline中有initcall_blacklist關鍵字時呼叫initcall_blacklist,也就是把字串以,分割後存入blacklisted_initcalls。 initcall_t是一個function pointer,透過sprint_symbol_no_offset查找function name。 12addr = (unsigned long) dereference_function_descriptor(fn);sprint_symbol_no_offset(fn_name, addr); 123456789101112131415161718#define dereference_function_descriptor(p) ((void *)(p))/** * sprint_symbol_no_offset - Look up a kernel symbol and return it in a text buffer * @buffer: buffer to be stored * @address: address to lookup * * This function looks up a kernel symbol with @address and stores its name * and module name to @buffer if possible. If no symbol was found, just saves * its @address as is. * * This function returns the number of bytes stored in @buffer. */int sprint_symbol_no_offset(char *buffer, unsigned long address){ return __sprint_symbol(buffer, address, 0, 0, 0);}EXPORT_SYMBOL_GPL(sprint_symbol_no_offset);","link":"/2024/06/07/module_init_and_init/"},{"title":"vdso and vvar","text":"使用/proc/self/maps時,會看到兩個maping 12ffffb3efc000-ffffb3efe000 r--p 00000000 00:00 0 [vvar]ffffb3efe000-ffffb3eff000 r-xp 00000000 00:00 0 [vdso] vDSO 全稱 virtual dynamic shared object,是一個由kernel自動map到所有user-space程序的shared library。其通常由C library調用,開發者不會直接使用它。vdso儲存指令,vvar儲存唯讀變數。 Why need vDSO有些kernel提供的syscall會在user-space中被頻繁呼叫,由於syscall會觸發中斷,CPU需要context switch並呼叫對應的ISR,因此在頻繁呼叫時會拖累performance,為了解決這種情況,vDSO將”not secret-any”及不須特權的資料直接放進memory,這就將本來需要syscall才能取得的資訊變成呼叫函式直接從memory中取出即可。一個例子是gettimeofday。 由vdso提供的symbol可在man page查看 Find the symbolvDSO的base address是randomized的,每次執行程序都產生不一樣的結果以避免”return-to-libc” issue。通常定義在vDSO中的symbol以__vdso__或__kernel_開頭。 strace and seccomp由於strace監控syscall,seccomp限制syscall,因此定義在vDSO內的symbol無法被兩者捕捉。 不過可以使用ltrace追蹤vdso。","link":"/2024/06/16/vdso_and_vvar/"},{"title":"用seccomp限制子程序系統呼叫","text":"最近eBPF非常火紅,在學習時發現了其前身cBPF,很多常用的工具如tcpdump都有用到,來學一下。 cBPF最初用於封包過濾,後來seccomp用其過濾器作規則配置,本篇就試著利用seccomp禁止子程序呼叫SYS_kill以及SYS_fchown。 對每一個syscall,seccomp會把其轉換成struct seccomp_data,宣告為 123456struct seccomp_data { int nr; __u32 arch; __u64 instruction_pointer; __u64 args[6];}; nr是指syscall numberarch是指令架構,我們需要檢查其值,因為不同架構syscall number會不一樣。 我們在寫規則時,要依據這個結構去存取資料 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273#include <stdlib.h>#include <stdio.h>#include <errno.h>#include <stddef.h>#include <unistd.h>#include <sys/prctl.h>#include <sys/syscall.h>#include <sys/wait.h>#include <linux/filter.h>#include <linux/seccomp.h>#include <linux/audit.h>struct sock_filter filter_inst[] = { // Load arch to reg. BPF_STMT(BPF_LD|BPF_W|BPF_ABS, (offsetof(struct seccomp_data, arch))), // If reg value is aarch64, goto next 2 inst, else goto next inst. BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, AUDIT_ARCH_AARCH64, 1, 0), // Invalid arch, just allow. BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_ALLOW), // Load syscall to reg. BPF_STMT(BPF_LD|BPF_W|BPF_ABS, offsetof(struct seccomp_data, nr)), // Check black list BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, SYS_kill, 2, 0), BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, SYS_fchown, 1, 0), // Allow. BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_ALLOW), // Deny. BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_KILL),};int main(int argc, char* argv[]) { struct sock_fprog prog = { .len = (unsigned short)(sizeof(filter_inst) / sizeof(filter_inst[0])), .filter = filter_inst, }; if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { return -1; } if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) { return -2; } int child_pid = fork(); if (child_pid == 0) { execvp(argv[1], argv+1); return -3; } int status = 0; if (waitpid(child_pid, &status, 0) < 0) { perror("waitpid fail\\n"); } if (WIFEXITED(status)) { printf("Exited normally with status %d\\n", WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { printf("Exited with signal %d\\n", WTERMSIG(status)); } else if (WIFSTOPPED(status)) { printf("Stopped with signal %d\\n", WSTOPSIG(status)); } else { perror("WIFEXITED"); } return 0;} 然後直接compile就行了。 1→ gcc main.c - o main 不在黑名單內的指令都可以正常執行 123→ ./main lsmain main.cExited normally with status 0 但遇到kill就不一樣了 12→ ./main kill -9 40382Exited with signal 31 可是chown不作用 12→ ./main chown zanets ./docExited normally with status 0 “It works, I don’t know why.” 用strace看看chown呼叫了什麼 12→ strace -n -f /usr/bin/chown zanets ./doc[ 54] fchownat(AT_FDCWD, "./doc", 1000, -1, 0) = 0 原來他使用的是fchownat,將其加進規則裡 1BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, SYS_fchownat, 3, 0), 編譯後執行,終於抓到他了。 12→ ./main chown zanets ./docExited with signal 31 補充PR_SET_NO_NEW_PRIVS白話就是 “no new privileges”,這會禁止設置該flag的thread所產生的子程序獲取新權限,例如該測試檔案這樣做的時候 1234→ ./main sudo lssudo: The "no new privileges" flag is set, which prevents sudo from running as root.sudo: If sudo is running in a container, you may need to adjust the container configuration to disable the flag.Exited normally with status 1 但讓parent擁有特權就沒問題 123→ sudo ./main lsmain main.cExited normally with status 0","link":"/2024/06/18/seccomp_syscall/"},{"title":"19. Remove Nth Node From End of List","text":"給予 head 做為 linked list 的開頭,刪掉倒數第 n 個節點。 直覺地解法就是走遍 list 取得總長度 m 後,就知道該刪掉第 m-n 個節點。 這是 two pass 的解法,有沒有可能做到 one pass? 使用 two pointer 就可做到,命名為 p1 和 p2。 讓 p1 走 n 步,此時 p1 距離開頭 n。 讓 p1 和 p2 一起走,直到 p1 抵達結尾。 p1 剩下的距離是 m-n,也是 p2 需要走的距離。 p2 的下一個點就是我們要刪除的節點。 有些人把這稱作 fast-slow pointer,讓我困惑不已。 1234567891011121314151617181920212223242526272829class Solution {public: ListNode* removeNthFromEnd(ListNode* head, int n) { ListNode* del = nullptr; ListNode* p1 = head, *p2 = head; for (int i = 0; i < n; i++) { p1 = p1->next; } if (p1 == nullptr) { delete(del); head = head->next; return head; } while (p1->next) { p1 = p1->next; p2 = p2->next; } ListNode* del = p2->next; p2->next = p2->next->next; delete(del); return head; }}; 細節第一個 for-loop 要不要i <= n,答案是不行,因為 loop 從 0 開始會多走一步變成 n+1,此時 p2 跑到剛剛好要刪的點。 如果要求刪除的點是 head,p1 會走遍 list 來到 nullptr,檢查 p1 是否為 nullptr 就可知道是要刪除 head。 第二個 while 檢查 p1->next 也是因為不希望多走一步變成 n+1。 Edge caselist 只有一個元素只有一個元素的時候,等於刪掉 head,p1 會走遍 list 來到 nullptr,檢查 p1 是否為 nullptr 就可知道是要刪除 head list 裡有兩個元素因為我們使用兩個 pointer,要考慮這種剛剛好的情況。 此時會有兩個可能:刪除 head 跟刪除 tail。 刪除 head 已經考慮過了;刪除 tail 時,p1 在 tail,p2 在 head,由於 p1->next 為 nullptr 第二個 while 不執行,p2 的下一節點為 tail,刪除下一個節點,回傳 head。","link":"/2024/06/28/Remove_nth_node_from_eol/"},{"title":"148. Sort List","text":"給與一個單向 linked list,將其從小到大排序。 先回想一下 merge sort 的作法,先將陣列不斷對半切,直到所有陣列都剩下一個元素,再將它們比大小後合併,也就是 divide and conquer。 我們學過如何使用 fast-slow 找中點,我們也學過如何從小到大 merge list,組合起來就可以實現 merge sort。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849class Solution {public: ListNode* mergeList(ListNode* l1, ListNode* l2) { ListNode head; ListNode* cur = &head; while(l1 && l2) { if (l1->val < l2->val) { cur->next = l1; l1 = l1->next; } else { cur->next = l2; l2 = l2->next; } cur = cur->next; } if (l1) cur->next = l1; if (l2) cur->next = l2; return head.next; } ListNode* sortList(ListNode* head) { if (head == nullptr || head->next == nullptr) { return head; } ListNode* slow = head; ListNode* fast = head; ListNode* tmp = nullptr; while(fast && fast->next) { fast = fast->next->next; tmp = slow; slow = slow->next; } // tmp is the tail of top half list // and slow is the head of bottom half tmp->next = nullptr; ListNode* l1 = sortList(head); ListNode* l2 = sortList(slow); return mergeList(l1, l2); }}; 複雜度時間 1234567891011121314151617181920212223242526272829T(1) = c'T(n) = n + 2T(n/2) + c"n展開 T(n/2)T(n) = n + 2[ n/2 + 2T(n/4) + c"n/2 ] + c"n = n + n + 4T(n/4) + c"n + c"n = 4T(n/4) + 2c"n + 2n展開 T(n/4)T(n) = 4[ n/4 + 2T(n/8) + c"n/4 ] + 2c"n + 2n = n + 8T(n/8) + c"n + 2c"n + 2n = 8T(n/8) + 3c"n +3n寫成一個general的形式T(n) = 2^kT(n/2^k) + kc"n + kn繼續簡化,由於已知T(1) = c',想將n/2^k轉成1的話n/2^k = 1n = 2^klogn = k,帶入T(n) = 2^logn*T(1) + logn*c"n + logn*n = nc' + c"nlogn + nlogn = nc' + (c" + 1) * nlognBigO 忽略常數項 nc' 及 c" + 1,所以是 O(nlogn)。 空間對 Linked List 分割時只做指標操作,但 recursive 需要空間存放 call stack,遞迴深度等於樹高,也就是 logN,所以用在 linked list 時空間複雜度 O(log n)。","link":"/2024/06/28/Sort%20List/"},{"title":"","text":"","link":"/2024/06/29/Time%20Complexity/"}],"tags":[{"name":"linux","slug":"linux","link":"/tags/linux/"},{"name":"c","slug":"c","link":"/tags/c/"},{"name":"debug","slug":"debug","link":"/tags/debug/"},{"name":"openwrt","slug":"openwrt","link":"/tags/openwrt/"},{"name":"security","slug":"security","link":"/tags/security/"},{"name":"seccomp","slug":"seccomp","link":"/tags/seccomp/"},{"name":"cBPF","slug":"cBPF","link":"/tags/cBPF/"},{"name":"Linked List","slug":"Linked-List","link":"/tags/Linked-List/"},{"name":"Leetcode","slug":"Leetcode","link":"/tags/Leetcode/"},{"name":"Sorting","slug":"Sorting","link":"/tags/Sorting/"}],"categories":[],"pages":[]}