函数的调用
计算机程序其实就是状态机,各个栈帧和全局变量构成状态,执行pc所指语句进行状态转换,调用函数时则把返回地址压入栈然后创建新的栈帧,汉诺塔的非递归实现即是使用显式栈帧管理来模拟递归调用栈。
编译器的优化
只要编译后的程序能够在任何输入下,得到与执行原程序语言等价的结果就行,于是有着很大优化空间。
- -O0 零级优化,已经有在做一些优化了,直接计算出常量表达式。
在今天并不需要学过所有结构,只要你学过任意一种体系结构或指令集架构,知道大概会有哪些指令,有些做访存、有些做算术、有些做调用,当你遇到一种你没学过的结构,直接让ai用你学过的来类比解释就可以了。
如何写一个最小的hello wrold
如果用objdump -d 查看我们通常第一次接触时写出的helloworld,在汇编代码中你会发现很多除了 printf 以外的指令。
helloworld: file format elf64-x86-64
Disassembly of section .init:
0000000000001000 <_init>:
1000: f3 0f 1e fa endbr64
1004: 48 83 ec 08 sub $0x8,%rsp
1008: 48 8b 05 d9 2f 00 00 mov 0x2fd9(%rip),%rax # 3fe8 <__gmon_start__@Base>
100f: 48 85 c0 test %rax,%rax
1012: 74 02 je 1016 <_init+0x16>
1014: ff d0 call *%rax
1016: 48 83 c4 08 add $0x8,%rsp
101a: c3 ret
Disassembly of section .plt:
0000000000001020 <.plt>:
1020: ff 35 9a 2f 00 00 push 0x2f9a(%rip) # 3fc0 <_GLOBAL_OFFSET_TABLE_+0x8>
1026: f2 ff 25 9b 2f 00 00 bnd jmp *0x2f9b(%rip) # 3fc8 <_GLOBAL_OFFSET_TABLE_+0x10>
102d: 0f 1f 00 nopl (%rax)
1030: f3 0f 1e fa endbr64
1034: 68 00 00 00 00 push $0x0
1039: f2 e9 e1 ff ff ff bnd jmp 1020 <_init+0x20>
103f: 90 nop
Disassembly of section .plt.got:
0000000000001040 <__cxa_finalize@plt>:
1040: f3 0f 1e fa endbr64
1044: f2 ff 25 ad 2f 00 00 bnd jmp *0x2fad(%rip) # 3ff8 <__cxa_finalize@GLIBC_2.2.5>
104b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
Disassembly of section .plt.sec:
0000000000001050 <printf@plt>:
1050: f3 0f 1e fa endbr64
1054: f2 ff 25 75 2f 00 00 bnd jmp *0x2f75(%rip) # 3fd0 <printf@GLIBC_2.2.5>
105b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
Disassembly of section .text:
0000000000001060 <_start>:
1060: f3 0f 1e fa endbr64
1064: 31 ed xor %ebp,%ebp
1066: 49 89 d1 mov %rdx,%r9
1069: 5e pop %rsi
106a: 48 89 e2 mov %rsp,%rdx
106d: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
1071: 50 push %rax
1072: 54 push %rsp
1073: 45 31 c0 xor %r8d,%r8d
1076: 31 c9 xor %ecx,%ecx
1078: 48 8d 3d ca 00 00 00 lea 0xca(%rip),%rdi # 1149 <main>
107f: ff 15 53 2f 00 00 call *0x2f53(%rip) # 3fd8 <__libc_start_main@GLIBC_2.34>
1085: f4 hlt
1086: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1)
108d: 00 00 00
0000000000001090 <deregister_tm_clones>:
1090: 48 8d 3d 79 2f 00 00 lea 0x2f79(%rip),%rdi # 4010 <__TMC_END__>
1097: 48 8d 05 72 2f 00 00 lea 0x2f72(%rip),%rax # 4010 <__TMC_END__>
109e: 48 39 f8 cmp %rdi,%rax
10a1: 74 15 je 10b8 <deregister_tm_clones+0x28>
10a3: 48 8b 05 36 2f 00 00 mov 0x2f36(%rip),%rax # 3fe0 <_ITM_deregisterTMCloneTable@Base>
10aa: 48 85 c0 test %rax,%rax
10ad: 74 09 je 10b8 <deregister_tm_clones+0x28>
10af: ff e0 jmp *%rax
10b1: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
10b8: c3 ret
10b9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
00000000000010c0 <register_tm_clones>:
10c0: 48 8d 3d 49 2f 00 00 lea 0x2f49(%rip),%rdi # 4010 <__TMC_END__>
10c7: 48 8d 35 42 2f 00 00 lea 0x2f42(%rip),%rsi # 4010 <__TMC_END__>
10ce: 48 29 fe sub %rdi,%rsi
10d1: 48 89 f0 mov %rsi,%rax
10d4: 48 c1 ee 3f shr $0x3f,%rsi
10d8: 48 c1 f8 03 sar $0x3,%rax
10dc: 48 01 c6 add %rax,%rsi
10df: 48 d1 fe sar %rsi
10e2: 74 14 je 10f8 <register_tm_clones+0x38>
10e4: 48 8b 05 05 2f 00 00 mov 0x2f05(%rip),%rax # 3ff0 <_ITM_registerTMCloneTable@Base>
10eb: 48 85 c0 test %rax,%rax
10ee: 74 08 je 10f8 <register_tm_clones+0x38>
10f0: ff e0 jmp *%rax
10f2: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
10f8: c3 ret
10f9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
0000000000001100 <__do_global_dtors_aux>:
1100: f3 0f 1e fa endbr64
1104: 80 3d 05 2f 00 00 00 cmpb $0x0,0x2f05(%rip) # 4010 <__TMC_END__>
110b: 75 2b jne 1138 <__do_global_dtors_aux+0x38>
110d: 55 push %rbp
110e: 48 83 3d e2 2e 00 00 cmpq $0x0,0x2ee2(%rip) # 3ff8 <__cxa_finalize@GLIBC_2.2.5>
1115: 00
1116: 48 89 e5 mov %rsp,%rbp
1119: 74 0c je 1127 <__do_global_dtors_aux+0x27>
111b: 48 8b 3d e6 2e 00 00 mov 0x2ee6(%rip),%rdi # 4008 <__dso_handle>
1122: e8 19 ff ff ff call 1040 <__cxa_finalize@plt>
1127: e8 64 ff ff ff call 1090 <deregister_tm_clones>
112c: c6 05 dd 2e 00 00 01 movb $0x1,0x2edd(%rip) # 4010 <__TMC_END__>
1133: 5d pop %rbp
1134: c3 ret
1135: 0f 1f 00 nopl (%rax)
1138: c3 ret
1139: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
0000000000001140 <frame_dummy>:
1140: f3 0f 1e fa endbr64
1144: e9 77 ff ff ff jmp 10c0 <register_tm_clones>
0000000000001149 <main>:
1149: f3 0f 1e fa endbr64
114d: 55 push %rbp
114e: 48 89 e5 mov %rsp,%rbp
1151: 48 8d 05 ac 0e 00 00 lea 0xeac(%rip),%rax # 2004 <_IO_stdin_used+0x4>
1158: 48 89 c7 mov %rax,%rdi
115b: b8 00 00 00 00 mov $0x0,%eax
1160: e8 eb fe ff ff call 1050 <printf@plt>
1165: b8 00 00 00 00 mov $0x0,%eax
116a: 5d pop %rbp
116b: c3 ret
Disassembly of section .fini:
000000000000116c <_fini>:
116c: f3 0f 1e fa endbr64
1170: 48 83 ec 08 sub $0x8,%rsp
1174: 48 83 c4 08 add $0x8,%rsp
1178: c3 ret
编译器或者说整个工具链处理程序时其实不是从main函数开始的,而是从 _start 开始跳转到一个pc初始化的代码,然后去调用 main 函数而main函数最终会返回,执行系统调用来清理掉这个进程以真正结束程序。因为c程序自己是不能结束程序执行的,操作系统才行。 _start 是在链接阶段由 gcc 加上。
系统调用指令syscall (x86-64), ecall (risc-v), svc (aarch64) 是区别于其他所有指令的特殊指令,执行后由操作系统接管程序,也是唯一打破程序状态边界与其他程序、设备交互的方式。而如果你想要正确地执行某个系统调用,你就要先把正确的值放到正确的寄存器里。
一个最小的helloworld
#include <sys/syscall.h>
.text
.global _start
_start:
# write(1, msg, msg_end - msg)
mov $SYS_write, %rax # rax = syscall number: write
mov $1, %rdi # rdi = fd: 1 stdout
lea msg(%rip), %rsi # rsi = buffer address
mov $(msg_end - msg), %rdx # rdx = length
syscall
# exit(0)
mov $SYS_exit, %rax # rax = syscall number: exit
mov $0, %rdi # rdi = exit code
syscall
.section .rodata
msg:
.ascii "HelloWorld\n"
msg_end:
能看到的幕后程序
Linux 系统通常默认的 GNU Coreutils,like ls,cp,mv,cat,sed等
不能看到的幕后程序
守护进程 deamon:在后台长期运行的进程
比如systemd(init系统和服务管理器)、cron、sshd、firewalld等
建立工具体系思维
使用 System call trace (strace) 去观察程序是如何与操作系统交互的,当然现在我们可以直接使用ai来完成
strace xedit时我们可以发现程序在不断的发消息和接收消息,所以应用程序做的事就是计算和使用操作系统api,而操作系统的职责正是提供令应用程序舒适的抽象 (对象 + API)。