Linux C 语言之 Hello World 详解
Linux C 语言之 Hello World 详解
第一个 C 语言程序
学习 C 语言,大多数接触的第一个 C 语言程序便是经典的 Hello World 程序,程序的功能是在当前终端上打印 “Hello World” 字符串!
该程序的实现代码如下:
#include <stdio.h>
void main()
{
printf("Hello World\n");
}
在 GNU/Linux 系统中,使用 gcc 编译器,编译并执行 helloworld 程序的指令为:
- 通过 vi 编辑器编写上面代码,并保存为 helloworld.c
- 使用 gcc 编译器编译源代码生成可执行文件 helloworld: gcc -o helloworld helloworld.c
- 执行当前目录中的 helloworld 程序:./helloworld
当前终端屏幕就会打印 Hello World,如下图:
程序运行原理
GNU/Linux 系统中可执行程序都是 elf 格式二进制文件,该文件跟 Windows 系统的 exe 文件类似,通过 Linux 的 Shell 比如 Bash 加载到内存,由操作系统启动
新线程,然后开始执行。我们可以通过 file 命令查看目标文件的格式:
:~$ file helloworld
helloworld: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=203388067920d237ab234e8eb97714f56919799f, not stripped
编译,链接
从源代码生成可执行文件,需要很多步骤,最主要的步骤就是编译和链接。在我们上述的过程中,编译和链接都是由 gcc 程序完成的。
当然我们也可以分开来执行编译和链接过程:
gcc -c helloworld.c
ld -o helloworld helloworld.o -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o /usr/lib/x86_64-linux-gnu/crtn.o -lc
可以看到,简单的 helloworld 程序依赖了大量的系统文件,其中主要的是程序运行环境相关的 crt (C RunTime Library)和 系统 c 语言库 glibc。
当然不同的平台这个步骤可能不同,可以在 gcc 命令中添加 -v 参数,查看编译和链接的完整步骤。
运行时
我们从代码可见的程序起始是 main 函数,但是编译器在编译链接的过程中,在我们的程序中添加了运行时代码,所以程序的起始并不是 main 函数了,可以通过 nm 查看我们的程序的地址和符号:
$ nm helloworld
0000000000600734 D __bss_start
0000000000600730 D __data_start
0000000000600730 W data_start
0000000000600570 d _DYNAMIC
0000000000600734 D _edata
0000000000600738 D _end
0000000000400464 T _fini
0000000000600708 d _GLOBAL_OFFSET_TABLE_
w __gmon_start__
0000000000400340 T _init
0000000000600570 d __init_array_end
0000000000600570 d __init_array_start
000000000040047c R _IO_stdin_used
0000000000400460 T __libc_csu_fini
00000000004003f0 T __libc_csu_init
U __libc_start_main@@GLIBC_2.2.5
00000000004003a0 T main
U puts@@GLIBC_2.2.5
00000000004003c0 T _start
可以看到 main 函数已经不是在程序的代码段开头了。可以通过对 gcc 添加 -Map 参数,来生成程序的 map 文件,方便我们查看程序的代码段,数据段等信息:
gcc -o helloworld helloworld.c -Wl,-Map,helloworld.map
通过 helloworld.map 可以清晰的看到 main 函数所在的 text 段,和相关的地址信息。
链接库
gcc 默认动态库的搜索路径搜索的先后顺序是:
- 编译目标代码时指定的动态库搜索路径;
- 环境变量LD_LIBRARY_PATH指定的动态库搜索路径;
- 配置文件/etc/ld.so.conf中指定的动态库搜索路径;
- 默认的动态库搜索路径/lib、/usr/lib。
所以指定目标库的时候需要使用 -rpath 参数传递路径给 gcc。
我们这里只是使用了标准 c 库,版本为 ldd 展示的 /lib/x86_64-linux-gnu/libc.so.6 GLIBC_2.2.5
ldd helloworld
linux-vdso.so.1 => (0x00007ffd493f3000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5f12756000)
/lib64/ld-linux-x86-64.so.2 (0x00007f5f12b20000)
编译器优化
我们显示调用的 c 库函数是 printf,在 c 语言库中 stdio.h 中定义:
/* Write formatted output to stdout.
This function is a possible cancellation point and therefore not
marked with __THROW. */
extern int printf (const char *__restrict __format, ...);
但是实际上,我们通过 nm 命令看到可执行文件中调用的 c 库的 puts, 通过汇编更能清晰的看到这个调用的详细情况:
gcc -S helloworld.c
cat helloworld.s
.file "helloworld.c"
.section .rodata
.LC0:
.string "Hello World"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $.LC0, %edi
call puts
nop
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.9) 5.4.0 20160609"
.section .note.GNU-stack,"",@progbits
当打印的全部是字符串,即没有需要转为字符串的操作的时候, gcc 会把 printf 优化成 puts。所以对于编译器的优化对程序员来说有时候是透明的。
我们需要仔细的检查编译器是否对我们的代码进行了优化。
Hello World 打印原理
从上面的分析,我们知道,我们的 helloworld 程序主要是调用了 puts 函数进行打印,puts 在 glibc 中的实现如下:
/* Write the string in S and a newline to stdout. */
int
puts (const char *s)
{
return fputs (s, stdout) || putchar ('\n') == EOF ? EOF : 0;
}
该函数主要是调用 fputs 将字符串送到 stdout (标注输出),并送出一个换行符!换行符同样是送到 stdout :
/* Write the character C on stdout. */
int
putchar (int c)
{
return __putc (c, stdout);
}
stdout, stdin 和 stderr
那么 stdout 是什么,glibc 是如何通过 stdout 将我们的终端相连接的呢?
stdout 在 glibc 中是 FILE 类型的指针:
/* Standard streams. */
extern FILE *stdin, *stdout, *stderr;
#ifdef __STRICT_ANSI__
/* ANSI says these are macros; satisfy pedants. */
#define stdin stdin
#define stdout stdout
#define stderr stderr
#endif
这 3 个指针分别是对应 fd 号为 0,1,2 的 3 个 标准 fd 的封装:
/* Standard streams. */
#define READ 1, 0
#define WRITE 0, 1
#define BUFFERED 0
#define UNBUFFERED 1
#define stdstream(name, next, fd, readwrite, unbuffered) \
{ \
_IOMAGIC, \
NULL, NULL, NULL, NULL, 0, \
(void *) fd, \
{ readwrite, /* ... */ }, \
{ NULL, NULL, NULL, NULL, NULL }, \
{ NULL, NULL }, \
-1, -1, \
(next), \
NULL, '\0', 0, \
0, 0, unbuffered, 0, 0, 0, 0 \
}
static FILE stdstreams[3] =
{
stdstream (&stdstreams[0], &stdstreams[1], STDIN_FILENO, READ, BUFFERED),
stdstream (&stdstreams[1], &stdstreams[2], STDOUT_FILENO, WRITE, BUFFERED),
stdstream (&stdstreams[2], NULL, STDERR_FILENO, WRITE, UNBUFFERED),
};
FILE *stdin = &stdstreams[0];
FILE *stdout = &stdstreams[1];
FILE *stderr = &stdstreams[2];
其中可以明确的知道:
- 只有 stderr 是不缓冲的,stdin 和 stdout 都是缓冲的,那么输出到 stdout 的字符可能不会立即显示
- stdin 是只读的, stdout 和 stderr 是只能写的,其他的操作,比如读 stdout 是不可预知的。
- fd 是显示直接强制赋值的,就是说 0,1,2 应该是已经打开的描述符,否则会出现输入输出错误。
那么是在何时打开的标准描述符呢?
stdio 与 tty
stdio 是与 tty 对应的,一个系统中可以有很多用户,或者一个用户打开了多个终端,但是 printf 等输出都是在当前终端上。
stdio 是与 tty 一一对应。从 glibc 的代码我们可以找到打开标准描述符 0,1,2 的位置:
login_tty.c
int
login_tty(fd)
int fd;
{
(void) setsid();
#ifdef TIOCSCTTY
if (ioctl(fd, TIOCSCTTY, (char *)NULL) == -1)
return (-1);
#else
{
/* This might work. */
char *fdname = ttyname (fd);
int newfd;
if (fdname)
{
if (fd != 0)
(void) close (0);
if (fd != 1)
(void) close (1);
if (fd != 2)
(void) close (2);
newfd = open (fdname, O_RDWR);
(void) close (newfd);
}
}
#endif
(void) dup2(fd, 0);
(void) dup2(fd, 1);
(void) dup2(fd, 2);
if (fd > 2)
(void) close(fd);
return (0);
}
每次登陆的时候,系统会将当前的 login 程序传入的 fb, dump 出来 3 份,分别的 fb 值就是 0,1,2
因此, stdin、stdout、stderr 其实对应的是同一个文件,这个文件就是当前 login 使用的 tty 。
从内存到设备
我们的 helloworld 程序被 shell 加载到内存, “Hello World” 字符串也是在内存的位置,如何输出到 tty 设备呢?
我们 tty 设备是虚拟的设备,可能是 LCD 显示器,可能是串口,也可能是 LED 显示器。其中的对应和输出流,
那就是要牵涉到具体的设备驱动,那又是另一个领域才能讲清楚的了。大概的数据流就是:
- 输出设备和 tty 是绑定的,输出到 tty 就会把数据传递给显示设备驱动程序
- 设备驱动程序会把字符串数据最后通过 DMA 或者其他总线方式发给设备
- 最终的设备会显示我们需要看到的字符串 “Hello World”
Linux C 语言之 Hello World 详解的更多相关文章
- 莱特币ltc在linux下的多种挖矿方案详解
莱特币ltc在linux下的多种挖矿方案详解 4.0.1 Nvidia显卡Linux驱动Nvidia全部驱动:http://www.nvidia.cn/Download/index.aspx?lang ...
- Linux Shell编程与编辑器使用详解
<Linux Shell编程与编辑器使用详解> 基本信息 作者: 刘丽霞 杨宇 出版社:电子工业出版社 ISBN:9787121207174 上架时间:2013-7-22 出版日期:201 ...
- R语言服务器程序 Rserve详解
R语言服务器程序 Rserve详解 R的极客理想系列文章,涵盖了R的思想,使用,工具,创新等的一系列要点,以我个人的学习和体验去诠释R的强大. R语言作为统计学一门语言,一直在小众领域闪耀着光芒.直到 ...
- 【Linux学习】Linux下用户组、文件权限详解
原文地址:http://www.cnblogs.com/123-/p/4189072.html Linux下用户组.文件权限详解 用户组 在linux中的每个用户必须属于一个组,不能独立于组外.在li ...
- LINUX的磁盘管理du命令详解
LINUX的磁盘管理du命令详解 du(disk usage)命令可以计算文件或目录所占的磁盘空间.没有指定任何选项时, 它会测量当前工作目录与其所有子目录,分别显示各个目录所占的快数,最后才显示工作 ...
- linux sort,uniq,cut,wc命令详解
linux sort,uniq,cut,wc命令详解 sort sort 命令对 File 参数指定的文件中的行排序,并将结果写到标准输出.如果 File 参数指定多个文件,那么 sort 命令将这些 ...
- linux mount命令参数及用法详解
linux mount命令参数及用法详解 非原创,主要来自 http://www.360doc.com/content/13/0608/14/12600778_291501907.shtml. htt ...
- Linux中/proc目录下文件详解
转载于:http://blog.chinaunix.net/uid-10449864-id-2956854.html Linux中/proc目录下文件详解(一)/proc文件系统下的多种文件提供的系统 ...
- 【转】linux expr命令参数及用法详解
在抓包过程中,查看某个设定时间内,数据上下行多少,用命令expr 计算! --------------------------------------------------------------- ...
随机推荐
- Foj 2299 Prefix(AC自动机、DP)
Foj 2299 Prefix 题意 给定串s.正整数n,问有多少长度为n的字符串t满足:s[0...i]是t的子串,s[0...i+1]不是. 题解 求有多少长度为n的字符串t满足:s[0...i] ...
- Centos7中yum安装MySQL
安装mysql [root@localhost ~]# yum update [root@localhost ~]# cat /etc/redhat-release CentOS Linux rele ...
- java String 常用方法集合
String a = "abc";String b = "abc";a==b ;//返回true,因为a,b指向的是同一个地址 String a = new S ...
- C语言基础笔试题一
1.下面的代码输出什么?为什么? void foo(void) { unsigned int a = 6; int b = -20; (a+b > 6)?puts(">6&quo ...
- Linux服务器磁盘空间占满问题
下面我们一起来看一篇关于Linux服务器磁盘占满问题解决(/dev/sda3 满了),希望碰到此类问题的人能带来帮助. 今天下班某电商技术部leader发现个问题,说他们服务器硬盘满了.把日志文件 ...
- 关于Mysql查询varchar类型错误问题
因为后台所有表ID都是按照雪花算法生成的18位数字,需要对接到Android,Ios和H5,此时H5会出现字符超长溢出,所以直接把ID改为varchar类型. 如我的一张表ID为varchar(18) ...
- 【[HEOI2014]大工程 】
可能是虚树板子题了 首先先把虚树建出来,但是这里和那道虚树的入门题不一样,这里所有的询问点都得在虚树里,所以不会存在那种直接不如栈的点 之后我们考虑一下这个三个要求的东西 第一个操作我们需要统计虚树上 ...
- TensorFlow学习之 图像预处理
import tensorflow as tf import matplotlib.pyplot as plt image_raw_data = tf.gfile.GFile("./pict ...
- NodeManager 启动一会儿挂掉
[root@hadoop1 hadoop-2.8.5]# less logs/yarn-root-nodemanager-hadoop1.log 查看日志发现 hostname配置错误 [root@ ...
- 【转】Java做服务器开发语言
版权声明:本文为博主原创文章,未经博主允许不得转载. 随着游戏市场的兴起,特别是网页游戏.手机游戏的崛起,对游戏开发技术的需求越来越多.网络游戏开发是一个庞大的体系,总体来说是客户端与服务器端.客户端 ...