? ? 曾幾何時,您有沒有在夜深人靜的時候想過一個問題,printf內部究竟做了什么?為何可以輸出到屏幕上顯示出來?
? ? 先看看這段熟悉的代碼:
? ?
//
// Created by xi.chen on 2017/9/2.
// Copyright ? 2017 All rights reserved.
//#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>int main()
{printf("hello, my cat!\n");return 0;
}
環境:
Mac OSX 10.12.3
Apple LLVM version 8.1.0 (clang-802.0.42)
Target: x86_64-apple-darwin16.4.0
Xcode 8.3.3
首先,我們先看看匯編代碼.
(__TEXT,__text) section
_main:
0000000100000f50 pushq %rbp
0000000100000f51 movq %rsp, %rbp
0000000100000f54 subq $0x10, %rsp
0000000100000f58 leaq 0x3b(%rip), %rdi ## literal pool for: "hello, my cat!\n"
0000000100000f5f movl $0x0, -0x4(%rbp)
0000000100000f66 movb $0x0, %al
0000000100000f68 callq 0x100000f7a ## symbol stub for: _printf
0000000100000f6d xorl %ecx, %ecx
0000000100000f6f movl %eax, -0x8(%rbp)
0000000100000f72 movl %ecx, %eax
0000000100000f74 addq $0x10, %rsp
0000000100000f78 popq %rbp
0000000100000f79 retq
可以看到,核心就是
callq 0x100000f7a ## symbol stub for: _printf
0000000100000f68
f68是在可執行文件的偏移.
可以用MachOView查看可執行文件的內部結構.
0000f60 45 fc 00 00 00 00 b0 00 e8 0d 00 00 00 31 c9 89
指令e8 0d 00 00 00 是call指令,相當于調用子程序,跳轉到當前指令后一條指令的PC值(0x100000f6d) + 偏移值(0d), 即0x100000f7a.
也就是上面callq之后的地址值.
上面沒有分析,0x100000000是什么?
xichen:hello xichen$ xcrun size -x -l -m !$
xcrun size -x -l -m /Users/xichen/Library/Developer/Xcode/DerivedData/hello-bkhrmjvnrfikgkfgkiemoyorgkig/Build/Products/Debug/hello
Segment __PAGEZERO: 0x100000000 (vmaddr 0x0 fileoff 0)
Segment __TEXT: 0x1000 (vmaddr 0x100000000 fileoff 0)Section __text: 0x2a (addr 0x100000f50 offset 3920)Section __stubs: 0x6 (addr 0x100000f7a offset 3962)Section __stub_helper: 0x1a (addr 0x100000f80 offset 3968)Section __cstring: 0x10 (addr 0x100000f9a offset 3994)Section __unwind_info: 0x48 (addr 0x100000fac offset 4012)total 0xa2
Segment __DATA: 0x1000 (vmaddr 0x100001000 fileoff 4096)Section __nl_symbol_ptr: 0x10 (addr 0x100001000 offset 4096)Section __la_symbol_ptr: 0x8 (addr 0x100001010 offset 4112)total 0x18
Segment __LINKEDIT: 0x3000 (vmaddr 0x100002000 fileoff 8192)
total 0x100005000
這個數值可以看成加載器把可執行文件加載到內存的虛擬地址基址. (上面的所有指令都是基于這個地址為基址)
回到callq這條指令,會跳轉到地址0x100000f7a,對應的指令是:
0000f70 45 f8 89 c8 48 83 c4 10 5d c3 ff 25 90 00 00 00
ff指令是jmp指令, 反匯編如下:
(lldb) x/3i 0x100000f7a0x100000f7a: ff 25 90 00 00 00 jmpq *0x90(%rip) ; (void *)0x0000000100000f900x100000f80: 4c 8d 1d 81 00 00 00 leaq 0x81(%rip), %r11 ; (void *)0x0000000000000000
jmpq跳轉到: 0xf80 + 0x90地址的數值為地址的地方.
>>> hex(0xf80+0x90)
'0x1010'
即跳轉到0x100001010.
(lldb) x/3g 0x100001010
0x100001010: 0x00007fffa8778180 0x0000000000000000
0x100001020: 0x0000000000000000
我們來看看printf的地址在哪里:
(lldb) dis -s printf
libsystem_c.dylib`printf:0x7fffa8778180 <+0>: pushq %rbp0x7fffa8778181 <+1>: movq %rsp, %rbp0x7fffa8778184 <+4>: pushq %r150x7fffa8778186 <+6>: pushq %r140x7fffa8778188 <+8>: pushq %rbx0x7fffa8778189 <+9>: subq $0xd8, %rsp0x7fffa8778190 <+16>: movq %rdi, %r140x7fffa8778193 <+19>: testb %al, %al0x7fffa8778195 <+21>: je 0x7fffa87781c3 ; <+67>0x7fffa8778197 <+23>: movaps %xmm0, -0xc0(%rbp)
0x7fffa8778180是不是和上面對上來了?
我們繼續dump printf后面調用了什么:
(lldb) x/50i 0x7fffa8778180
...............0x7fffa8778235: 48 0f 45 f0 cmovneq %rax, %rsi0x7fffa8778239: 48 8d 4d c0 leaq -0x40(%rbp), %rcx0x7fffa877823d: 48 89 df movq %rbx, %rdi0x7fffa8778240: 4c 89 f2 movq %r14, %rdx0x7fffa8778243: e8 c0 20 00 00 callq 0x7fffa877a308 ; vfprintf_l0x7fffa8778248: 4c 3b 7d e0 cmpq -0x20(%rbp), %r150x7fffa877824c: 75 0e jne
我們有幸可以看到mac開放的libc源代碼:
int
printf(char const * __restrict fmt, ...)
{int ret;va_list ap;va_start(ap, fmt);ret = vfprintf_l(stdout, __current_locale(), fmt, ap);va_end(ap);return (ret);
}
調用vfprintf_l, 是不是感覺一切都在預期之內呢?
繼續在libc跟蹤一番,會發現最終會調用write系統調用完成.
write系統調用會使用int指令陷入內核,執行寫數據的操作.
到此,您會不會有疑問,為何調用printf函數中跳轉了好多次,是因為編譯系統傻嗎?當然不是,因為采用的是動態鏈接庫, 主程序一開始并不知道調用的printf函數最終會在哪個地址,所以先保留了一個stub,等加載器加載運行時,再填入對應的地址.
就是上面的jmp跳轉所實現的, 而call printf這個語句并不需要等運行時再計算地址,編譯期就可以用此時設定的固定地址.
至此,我們已經理清了上面的flow. 那又是如何顯示在屏幕上的呢?
如果從終端terminal開始,調用了上面的應用程序(比如hello), ?terminal會fork一個進程, 并執行hello,然后等待hello完成 (此種是不帶后臺運行的模式).
hello調用了printf輸出,printf是向stdout輸出,為何向stdout會在此terminal上顯示呢?
首先我們要明白,stdout究竟指向哪個設備?
xichen:hello xichen$ tty
/dev/ttys001
所以,printf其實是向/dev/ttys001設備去寫.?
對應kernel的代碼:
/** ttwrite (LDISC)** Process a write call on a tty device.** Locks: Assumes tty_lock() is held prior to calling.*/
int
ttwrite(struct tty *tp, struct uio *uio, int flag)
終端會在tty有數據的時候,把數據畫到屏幕上. (注意: printf后面的字符串是終端進程畫到屏幕上的,不是hello畫的,因為hello只是寫文件,寫文件當然不一定會顯示到屏幕, 只是一般腦袋瓜子正常的終端都會回顯對應的文本信息).
至于,如何把一段文本畫到屏幕上,這個就不用多說了.
?
微風不燥,陽光正好,你就像風一樣經過這里,愿你停留的片刻溫暖舒心。
我是程序員小迷(致力于C、C++、Java、Kotlin、Android、Shell、JavaScript、TypeScript、Python等編程技術的技巧經驗分享),若作品對您有幫助,請關注、分享、點贊、收藏、在看、喜歡,您的支持是我們為您提供幫助的最大動力。
歡迎關注。助您在編程路上越走越好!