Linux C 语言之 Hello World 详解

第一个 C 语言程序

学习 C 语言,大多数接触的第一个 C 语言程序便是经典的 Hello World 程序,程序的功能是在当前终端上打印 “Hello World” 字符串!

该程序的实现代码如下:

  1. #include <stdio.h>
  2. void main()
  3. {
  4. printf("Hello World\n");
  5. }

在 GNU/Linux 系统中,使用 gcc 编译器,编译并执行 helloworld 程序的指令为:

  1. 通过 vi 编辑器编写上面代码,并保存为 helloworld.c
  2. 使用 gcc 编译器编译源代码生成可执行文件 helloworld: gcc -o helloworld helloworld.c
  3. 执行当前目录中的 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

  1. 0000000000600734 D __bss_start
  2. 0000000000600730 D __data_start
  3. 0000000000600730 W data_start
  4. 0000000000600570 d _DYNAMIC
  5. 0000000000600734 D _edata
  6. 0000000000600738 D _end
  7. 0000000000400464 T _fini
  8. 0000000000600708 d _GLOBAL_OFFSET_TABLE_
  9. w __gmon_start__
  10. 0000000000400340 T _init
  11. 0000000000600570 d __init_array_end
  12. 0000000000600570 d __init_array_start
  13. 000000000040047c R _IO_stdin_used
  14. 0000000000400460 T __libc_csu_fini
  15. 00000000004003f0 T __libc_csu_init
  16. U __libc_start_main@@GLIBC_2.2.5
  17. 00000000004003a0 T main
  18. U puts@@GLIBC_2.2.5
  19. 00000000004003c0 T _start

可以看到 main 函数已经不是在程序的代码段开头了。可以通过对 gcc 添加 -Map 参数,来生成程序的 map 文件,方便我们查看程序的代码段,数据段等信息:

gcc -o helloworld helloworld.c -Wl,-Map,helloworld.map

通过 helloworld.map 可以清晰的看到 main 函数所在的 text 段,和相关的地址信息。

链接库

gcc 默认动态库的搜索路径搜索的先后顺序是:

  1. 编译目标代码时指定的动态库搜索路径;
  2. 环境变量LD_LIBRARY_PATH指定的动态库搜索路径;
  3. 配置文件/etc/ld.so.conf中指定的动态库搜索路径;
  4. 默认的动态库搜索路径/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 中定义:

  1. /* Write formatted output to stdout.
  2. This function is a possible cancellation point and therefore not
  3. marked with __THROW. */
  4. extern int printf (const char *__restrict __format, ...);

但是实际上,我们通过 nm 命令看到可执行文件中调用的 c 库的 puts, 通过汇编更能清晰的看到这个调用的详细情况:

gcc -S helloworld.c

cat helloworld.s

  1. .file "helloworld.c"
  2. .section .rodata
  3. .LC0:
  4. .string "Hello World"
  5. .text
  6. .globl main
  7. .type main, @function
  8. main:
  9. .LFB0:
  10. .cfi_startproc
  11. pushq %rbp
  12. .cfi_def_cfa_offset 16
  13. .cfi_offset 6, -16
  14. movq %rsp, %rbp
  15. .cfi_def_cfa_register 6
  16. movl $.LC0, %edi
  17. call puts
  18. nop
  19. popq %rbp
  20. .cfi_def_cfa 7, 8
  21. ret
  22. .cfi_endproc
  23. .LFE0:
  24. .size main, .-main
  25. .ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.9) 5.4.0 20160609"
  26. .section .note.GNU-stack,"",@progbits

当打印的全部是字符串,即没有需要转为字符串的操作的时候, gcc 会把 printf 优化成 puts。所以对于编译器的优化对程序员来说有时候是透明的。

我们需要仔细的检查编译器是否对我们的代码进行了优化。

Hello World 打印原理

从上面的分析,我们知道,我们的 helloworld 程序主要是调用了 puts 函数进行打印,puts 在 glibc 中的实现如下:

  1. /* Write the string in S and a newline to stdout. */
  2. int
  3. puts (const char *s)
  4. {
  5. return fputs (s, stdout) || putchar ('\n') == EOF ? EOF : 0;
  6. }

该函数主要是调用 fputs 将字符串送到 stdout (标注输出),并送出一个换行符!换行符同样是送到 stdout :

  1. /* Write the character C on stdout. */
  2. int
  3. putchar (int c)
  4. {
  5. return __putc (c, stdout);
  6. }

stdout, stdin 和 stderr

那么 stdout 是什么,glibc 是如何通过 stdout 将我们的终端相连接的呢?

stdout 在 glibc 中是 FILE 类型的指针:

  1. /* Standard streams. */
  2. extern FILE *stdin, *stdout, *stderr;
  3. #ifdef __STRICT_ANSI__
  4. /* ANSI says these are macros; satisfy pedants. */
  5. #define stdin stdin
  6. #define stdout stdout
  7. #define stderr stderr
  8. #endif

这 3 个指针分别是对应 fd 号为 0,1,2 的 3 个 标准 fd 的封装:

  1. /* Standard streams. */
  2. #define READ 1, 0
  3. #define WRITE 0, 1
  4. #define BUFFERED 0
  5. #define UNBUFFERED 1
  6. #define stdstream(name, next, fd, readwrite, unbuffered) \
  7. { \
  8. _IOMAGIC, \
  9. NULL, NULL, NULL, NULL, 0, \
  10. (void *) fd, \
  11. { readwrite, /* ... */ }, \
  12. { NULL, NULL, NULL, NULL, NULL }, \
  13. { NULL, NULL }, \
  14. -1, -1, \
  15. (next), \
  16. NULL, '\0', 0, \
  17. 0, 0, unbuffered, 0, 0, 0, 0 \
  18. }
  19. static FILE stdstreams[3] =
  20. {
  21. stdstream (&stdstreams[0], &stdstreams[1], STDIN_FILENO, READ, BUFFERED),
  22. stdstream (&stdstreams[1], &stdstreams[2], STDOUT_FILENO, WRITE, BUFFERED),
  23. stdstream (&stdstreams[2], NULL, STDERR_FILENO, WRITE, UNBUFFERED),
  24. };
  25. FILE *stdin = &stdstreams[0];
  26. FILE *stdout = &stdstreams[1];
  27. FILE *stderr = &stdstreams[2];

其中可以明确的知道:

  1. 只有 stderr 是不缓冲的,stdin 和 stdout 都是缓冲的,那么输出到 stdout 的字符可能不会立即显示
  2. stdin 是只读的, stdout 和 stderr 是只能写的,其他的操作,比如读 stdout 是不可预知的。
  3. fd 是显示直接强制赋值的,就是说 0,1,2 应该是已经打开的描述符,否则会出现输入输出错误。

那么是在何时打开的标准描述符呢?

stdio 与 tty

stdio 是与 tty 对应的,一个系统中可以有很多用户,或者一个用户打开了多个终端,但是 printf 等输出都是在当前终端上。

stdio 是与 tty 一一对应。从 glibc 的代码我们可以找到打开标准描述符 0,1,2 的位置:

login_tty.c

  1. int
  2. login_tty(fd)
  3. int fd;
  4. {
  5. (void) setsid();
  6. #ifdef TIOCSCTTY
  7. if (ioctl(fd, TIOCSCTTY, (char *)NULL) == -1)
  8. return (-1);
  9. #else
  10. {
  11. /* This might work. */
  12. char *fdname = ttyname (fd);
  13. int newfd;
  14. if (fdname)
  15. {
  16. if (fd != 0)
  17. (void) close (0);
  18. if (fd != 1)
  19. (void) close (1);
  20. if (fd != 2)
  21. (void) close (2);
  22. newfd = open (fdname, O_RDWR);
  23. (void) close (newfd);
  24. }
  25. }
  26. #endif
  27. (void) dup2(fd, 0);
  28. (void) dup2(fd, 1);
  29. (void) dup2(fd, 2);
  30. if (fd > 2)
  31. (void) close(fd);
  32. return (0);
  33. }

每次登陆的时候,系统会将当前的 login 程序传入的 fb, dump 出来 3 份,分别的 fb 值就是 0,1,2

因此, stdin、stdout、stderr 其实对应的是同一个文件,这个文件就是当前 login 使用的 tty 。

从内存到设备

我们的 helloworld 程序被 shell 加载到内存, “Hello World” 字符串也是在内存的位置,如何输出到 tty 设备呢?

我们 tty 设备是虚拟的设备,可能是 LCD 显示器,可能是串口,也可能是 LED 显示器。其中的对应和输出流,

那就是要牵涉到具体的设备驱动,那又是另一个领域才能讲清楚的了。大概的数据流就是:

  1. 输出设备和 tty 是绑定的,输出到 tty 就会把数据传递给显示设备驱动程序
  2. 设备驱动程序会把字符串数据最后通过 DMA 或者其他总线方式发给设备
  3. 最终的设备会显示我们需要看到的字符串 “Hello World”

Linux C 语言之 Hello World 详解的更多相关文章

  1. 莱特币ltc在linux下的多种挖矿方案详解

    莱特币ltc在linux下的多种挖矿方案详解 4.0.1 Nvidia显卡Linux驱动Nvidia全部驱动:http://www.nvidia.cn/Download/index.aspx?lang ...

  2. Linux Shell编程与编辑器使用详解

    <Linux Shell编程与编辑器使用详解> 基本信息 作者: 刘丽霞 杨宇 出版社:电子工业出版社 ISBN:9787121207174 上架时间:2013-7-22 出版日期:201 ...

  3. R语言服务器程序 Rserve详解

    R语言服务器程序 Rserve详解 R的极客理想系列文章,涵盖了R的思想,使用,工具,创新等的一系列要点,以我个人的学习和体验去诠释R的强大. R语言作为统计学一门语言,一直在小众领域闪耀着光芒.直到 ...

  4. 【Linux学习】Linux下用户组、文件权限详解

    原文地址:http://www.cnblogs.com/123-/p/4189072.html Linux下用户组.文件权限详解 用户组 在linux中的每个用户必须属于一个组,不能独立于组外.在li ...

  5. LINUX的磁盘管理du命令详解

    LINUX的磁盘管理du命令详解 du(disk usage)命令可以计算文件或目录所占的磁盘空间.没有指定任何选项时, 它会测量当前工作目录与其所有子目录,分别显示各个目录所占的快数,最后才显示工作 ...

  6. linux sort,uniq,cut,wc命令详解

    linux sort,uniq,cut,wc命令详解 sort sort 命令对 File 参数指定的文件中的行排序,并将结果写到标准输出.如果 File 参数指定多个文件,那么 sort 命令将这些 ...

  7. linux mount命令参数及用法详解

    linux mount命令参数及用法详解 非原创,主要来自 http://www.360doc.com/content/13/0608/14/12600778_291501907.shtml. htt ...

  8. Linux中/proc目录下文件详解

    转载于:http://blog.chinaunix.net/uid-10449864-id-2956854.html Linux中/proc目录下文件详解(一)/proc文件系统下的多种文件提供的系统 ...

  9. 【转】linux expr命令参数及用法详解

    在抓包过程中,查看某个设定时间内,数据上下行多少,用命令expr 计算! --------------------------------------------------------------- ...

随机推荐

  1. Navicat for mysql 导出导入的问题

    问题现象   1:使用navicat 导出5.6.20版本数据库,然后导入到5.7.19mysql出现错误:  

  2. Android开发(7)数据库和Content Provider

    问题聚焦: 思想:应用程序数据的共享 对数据库的访问仅限于创建它的应用程序,但是事情不是绝对的 Content Provider提供了一个标准的接口,可供其他应用程序访问和使用其他程序的数据 下面我们 ...

  3. 沉淀再出发:dubbo的基本原理和应用实例

    沉淀再出发:dubbo的基本原理和应用实例 一.前言 阿里开发的dubbo作为服务治理的工具,在分布式开发中有着重要的意义,这里我们主要专注于dubbo的架构,基本原理以及在Windows下面开发出来 ...

  4. ZT android -- 蓝牙 bluetooth (四)OPP文件传输

    android -- 蓝牙 bluetooth (四)OPP文件传输 分类: Android的原生应用分析 2013-06-22 21:51 2599人阅读 评论(19) 收藏 举报 4.2源码AND ...

  5. 移动App中常见的Web漏洞

    智能手机的存在让网民的生活从PC端开始往移动端转向,现在网民的日常生活需求基本上一部手机就能解决.外卖,办公,社交,银行转账等等都能通过移动端App实现.那么随之也带来了很多信息安全问题,大量的用户信 ...

  6. Linux下文件的打包、解压缩指令——tar,gzip,bzip2,unzip,rar

    本文是笔者对鸟叔的Linux私房菜(基础学习篇) 第三版(中文网站)中关于 Linux 环境下打包和解压缩指令的内容以及日常操作过程中所接触的相关指令的总结和记录,以供备忘和分享.更多详细信息可直接参 ...

  7. 第04章-VTK基础(7)

    [译者:这个系列教程是以Kitware公司出版的<VTK User's Guide -11th edition>一书作的中文翻译(出版时间2010年.ISBN: 978-1-930934- ...

  8. P1081 开车旅行

    题目描述 小 A 和小 B 决定利用假期外出旅行,他们将想去的城市从 1 到 N 编号,且编号较小的 城市在编号较大的城市的西边,已知各个城市的海拔高度互不相同,记城市 i 的海拔高度为 Hi,城市 ...

  9. Web项目打成war包部署到tomcat时报MySQL Access denied for user 'root'@'localhost' (using password: YES)错误解决方案

    Web项目使用使用root账号root密码进行部署,通过Eclipse加载到Tomcat服务器可以发布成功,打成war包放到tomcat的webapps目录无法发布成功,报错: jdbc.proper ...

  10. Kali-linux使用Metasploitable操作系统

    Metasploitable是一款基于Ubuntu Linux的操作系统.该系统是一个虚拟机文件,从http://sourceforge.net/projects/metasploitable/fil ...