printf内幕----编程内幕(1)

    曾几何时,您有没有在夜深人静的时候想过一个问题,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 0x100000f7a
    0x100000f7a: ff 25 90 00 00 00     jmpq   *0x90(%rip)               ; (void *)0x0000000100000f90
    0x100000f80: 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  %rbp
    0x7fffa8778181 <+1>:  movq   %rsp, %rbp
    0x7fffa8778184 <+4>:  pushq  %r15
    0x7fffa8778186 <+6>:  pushq  %r14
    0x7fffa8778188 <+8>:  pushq  %rbx
    0x7fffa8778189 <+9>:  subq   $0xd8, %rsp
    0x7fffa8778190 <+16>: movq   %rdi, %r14
    0x7fffa8778193 <+19>: testb  %al, %al
    0x7fffa8778195 <+21>: je     0x7fffa87781c3            ; <+67>
    0x7fffa8778197 <+23>: movaps %xmm0, -0xc0(%rbp)


0x7fffa8778180是不是和上面对上来了?
我们继续dump printf后面调用了什么:

(lldb)  x/50i 0x7fffa8778180
...............
 
    0x7fffa8778235: 48 0f 45 f0           cmovneq %rax, %rsi
    0x7fffa8778239: 48 8d 4d c0           leaq   -0x40(%rbp), %rcx
    0x7fffa877823d: 48 89 df              movq   %rbx, %rdi
    0x7fffa8778240: 4c 89 f2              movq   %r14, %rdx
    0x7fffa8778243: e8 c0 20 00 00        callq  0x7fffa877a308            ; vfprintf_l
    0x7fffa8778248: 4c 3b 7d e0           cmpq   -0x20(%rbp), %r15
    0x7fffa877824c: 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等编程技术的技巧经验分享),若作品对您有帮助,请关注、分享、点赞、收藏、在看、喜欢,您的支持是我们为您提供帮助的最大动力。

欢迎关注。助您在编程路上越走越好!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/758906.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于机器学习的制冷系统过充电和欠充电故障诊断(采用红外热图像数据,MATLAB)

到目前为止&#xff0c;制冷系统故障诊断方法已经产生很多种&#xff0c;概括起来主要有三大类&#xff1a;基于分析的方法&#xff0c;基于知识的方法和基于数据驱动的方法。基于分析的方法主要获得制冷系统的数学模型&#xff0c;通过残差来检测和诊断故障。如果存在残差且很…

SonicSense:声学振动丰富机器人的物体感知能力

在通过声学振动进行物体感知方面&#xff0c;尽管以往的研究已经取得了一些有希望的结果&#xff0c;但目前的解决方案仍然受限于几个方面。首先&#xff0c;大多数现有研究集中在只有少数&#xff08;N < 5&#xff09;基本物体的受限设置上。这些物体通常具有均质材料组成…

电路笔记(电源模块): 基于FT2232HL实现的jtag下载器硬件+jtag的通信引脚说明

JTAG接口说明 JTAG 接口根据需求可以选择20针或14针的配置&#xff0c;具体选择取决于应用场景和需要连接的功能。比如之前的可编程逻辑器件XC9572XL使用JTAG引脚&#xff08;TCK、TDI、TDO、TMS、VREF、GND&#xff09;用于与器件进行调试和编程通信。更详细的内容可以阅读11…

KAIROS复现记录

KAIROS:使用全系统起源的实用入侵检测和调查 Github&#xff1a;https://github.com/ProvenanceAnalytics/kairos KAIROS: Practical Intrusion Detection and Investigation using Whole-system Provenance 1. 论文实验 实验部分使用SCISKIT-LEARN来实现分层特征散列&#xf…

硬核!大佬通过Intel CPU的JTAG接口,DUMP微软原始Xbox的加密BootROM。

这是一篇记录如何通过Intel CPU的JTAG接口,DUMP微软原始Xbox的加密BootROM的文章,内容也记录了老哥如何设计实现JTAG调试器的过程,非常硬核! 原文:JTAG ‘Hacking’ the Original Xbox in 2023 Using Intel CPU JTAG to dump the secret bootrom in Microsoft’s original…

Java代码基础算法练习-求成绩单中最高和第二高的成绩-2024.06.30

任务描述&#xff1a; 输入n(0<n<20)个整数代表成绩&#xff0c;求n个成绩中最高的和第二高成绩 解决思路&#xff1a; 输入的数字 n 为 for 循环的次数&#xff0c;在每次循环中进行值的输入和判断 如果当前输入的分数大于最大值&#xff0c;则更新最大值和次大值 如…

Golang-channel理解

channel golang-channel语雀笔记整理 channelgolang channel的设计动机&#xff1f;chanel的数据结构/设计思考 golang channel的设计动机&#xff1f; channel是一种不同协程之间实现异步通信的数据结构。golang中有一种很经典的说法是要基于通信实现共享内存&#xff0c;而不…

grpc教程——proto文件转go

【1】编写一个proto文件 syntax "proto3"; package myproto;service NC{rpc SayStatus (NCRequest) returns (NCResponse){} }message NCRequest{ string name 1; } message NCResponse{string status 1; } 【2】转换&#xff1a;protoc --go_out. myservice.pro…

重生奇迹MU 正确获取金币的方式

在游戏中&#xff0c;需要消耗大量的金币来购买红药等物品。因此&#xff0c;如何快速赚取金币也成为玩家关注的问题。您知道有哪些方法可以快速地获得金币吗&#xff1f; 一、哪个地图上是最适合打金币的很关键 在选择打钱的地方时&#xff0c;不能盲目行动&#xff0c;需要…

安装maven与nexus

安装maven与nexus Maven官网下载地址&#xff1a;http://maven.apache.org cd /data/software/wget https://mirrors.tuna.tsinghua.edu.cn/apache/maven/maven-3/3.8.1/binaries/apache-maven-3.8.8-bin.tar.gz# 解压 tar xf apache-maven-3.8.1-bin.tar.gz -C /opt/[rooth…

木各力“GERRI”被“GREE”格力无效宣告成功

近日“GERRI”被“GREE”格力无效宣告成功&#xff0c;“GERRI”和“GREE”近似不&#xff0c;如果很近似当初就不会通过初审和下商标注册证&#xff0c;但是如果涉及知名商标和驰名商标&#xff0c;人家就可以异议和无效。 “GERRI”在被无效宣告时&#xff0c;引用了6个相关的…

【启明智显分享】乐鑫ESP32-S3R8方案2.8寸串口屏:高性能低功耗,WIFI/蓝牙无线通信

近年来HMI已经成为大量应用聚焦的主题&#xff0c;在消费类产品通过创新的HMI设计带来增强的连接性和更加身临其境的用户体验之际&#xff0c;工业产品却仍旧在采用物理接口。这些物理接口通常依赖小型显示器或是简单的LED&#xff0c;通过简单的机电开关或按钮来实现HMI交互。…

竞赛 深度学习 大数据 股票预测系统 - python lstm

文章目录 0 前言1 课题意义1.1 股票预测主流方法 2 什么是LSTM2.1 循环神经网络2.1 LSTM诞生 2 如何用LSTM做股票预测2.1 算法构建流程2.2 部分代码 3 实现效果3.1 数据3.2 预测结果项目运行展示开发环境数据获取 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天…

2.2 Python数据类型详解

第二节&#xff1a;Python数据类型详解 Python作为一种动态类型语言&#xff0c;支持多种数据类型&#xff0c;每种数据类型都有其特定的特点和用途。本章将详细介绍Python中常见的数据类型及其特性&#xff0c;以及如何使用这些数据类型进行编程。 2.2.1 整数 (int) 整数是…

黑马点评-Redis的缓存击穿,缓存雪崩,缓存穿透,互斥锁

文章目录 1.缓存穿透2.缓存雪崩3.缓存击穿3.1 互斥锁 1.缓存穿透 解决办法 写入NULL值到Redis缓存&#xff0c;以后就会命中Redis的控制缓存而不会出现请求直接打到数据库的问题&#xff01; 代码 2.缓存雪崩 这个概念很好理解&#xff0c;雪崩就是无数的小雪花结构突然因…

pandas数据分析(1)

pandas&#xff0c;即Python数据分析库&#xff08;Python data analysis library&#xff09; DataFrame和Series DataFrame&#xff08;数据帧&#xff09;和Series&#xff08;序列&#xff09;是pandas的核心数据结构。DataFrame的主要组件包含索引、列、数据。DataFrame和…

扫描全能王的AI驱动创新与智能高清滤镜技术解析

目录 引言1、扫描全能王2、智能高清滤镜黑科技2.1、图像视觉矫正2.2、去干扰技术 3、实际应用案例3.1、打印文稿褶皱检测3.2、试卷擦除手写3.3、老旧文件处理3.4、收银小票3.5、从不同角度扫描文档 4、用户体验结论与未来展望 引言 在数字化时代背景下&#xff0c;文档扫描功能…

云计算【第一阶段(21)】Linux引导过程与服务控制

目录 一、linux操作系统引导过程 1.1、开机自检 1.2、MBR引导 1.3、GRUB菜单 1.4、加载 Linux 内核 1.5、init进程初始化 1.6、简述总结 1.7、初始化进程centos 6和7的区别 二、排除启动类故障 2.1、修复MBR扇区故障 2.1.1、 实验 2.2、修复grub引导故障 2.2.1、实…

从AICore到TensorCore:华为910B与NVIDIA A100全面分析

华为NPU 910B与NVIDIA GPU A100性能对比&#xff0c;从AICore到TensorCore&#xff0c;展现各自计算核心优势。 AI 2.0浪潮汹涌而来&#xff0c;若仍将其与区块链等量齐观&#xff0c;视作炒作泡沫&#xff0c;则将错失新时代的巨大机遇。现在&#xff0c;就是把握AI时代的关键…

深入解析高斯过程:数学理论、重要概念和直观可视化全解

与其他算法相比&#xff0c;高斯过程不那么流行&#xff0c;但是如果你只有少量的数据&#xff0c;那么可以首先高斯过程。在这篇文章中&#xff0c;我将详细介绍高斯过程。并可视化和Python实现来解释高斯过程的数学理论。 多元高斯分布 多元高斯分布是理解高斯过程所必须的概…