专栏首页LINUX阅码场吴章金: 实例解析 Linux C 语言程序之变量类型

吴章金: 实例解析 Linux C 语言程序之变量类型


license: "cc-by-nc-nd-4.0"

"本文从编译、二进制程序文件和运行角度逐级解析了 Linux C 语言程序中几种变量类型"

吴章金老师《360度剖析Linux ELF系列文章》:

吴章金:如何创建一个*可执行*的共享库

吴章金: 深度剖析 Linux共享库的“位置无关”实现原理

吴章金:通过操作 Section 为 Linux ELF 程序新增数据

背景说明

前几天,有同学在 “泰晓原创团队” 讨论群问道:

请教下,谭 C,8.9.3,用 static 声明静态局部变量,在实际中可有案例。

看到这个问题,立即浮现的概念是 RUN ONCE,内核源码找了一下:

$ grep -i "static.*run_once" -ur ./ --include "*.c"
./arch/mips/mm/page.c:  static atomic_t run_once = ATOMIC_INIT(0);
./arch/mips/mm/page.c:  static atomic_t run_once = ATOMIC_INIT(0);
./arch/mips/mm/tlbex.c: static int run_once = 0;

代码示例1:

void build_clear_page(void)
{
  static atomic_t run_once = ATOMIC_INIT(0);

  if (atomic_xchg(&run_once, 1)) {
    return;
  }
  /* body */
}

代码示例2:

void build_tlb_refill_handler(void)
{
  ...
  if (!run_once) {
    /* body */
    run_once++;
  }
}

另外,他又继续问道:

一般是用 static 定义一个全局的,很少看到函数内部在用 static?

这个问题则上升到 C 语言关键字 static 的用法。

static 这个关键字用来限定某个变量或者函数的作用域,这个作用域可能是文件层面,也可能是函数层面。

从语法的角度去解释某个关键字用法的文章很多,可是这些解释蛮多时候是很生硬的,不是那么好记忆。

本文尝试从实操的角度去解析 static 以及更多类型的 C 语言变量的形态。

从编译的角度

假如某个功能需求由多个文件构成如下:

$ cat print.h
extern void print(char *str);
extern char *hello;

$ cat hello.c
#include "print.h"

int main(void)
{
  print(hello);
  return 0;
}

$ cat print.c
#include <stdio.h>

char *hello = "hello";

void print(char *str);
{
  printf("%s\n", str);
}

编译运行如下:

$ gcc -m32 -o hello x.c print.h print.c
$ ./hello
hello

类似这种需要跨文件访问的函数和变量,如果定义成 static 的话:

$ cat print.c
#include <stdio.h>

static char *hello = "hello";

static void print(char *str);
{
  printf("%s\n", str);
}

$ gcc -m32 -o hello x.c print.h print.c
/tmp/ccetJaG2.o: In function `main':
x.c:(.text+0x12): undefined reference to `hello'
x.c:(.text+0x1b): undefined reference to `print'
collect2: error: ld returned 1 exit status

从 ELF 二进制程序文件的角度

先来编译成一个中间的可重定位文件:

$ gcc -m32 -c -o print.o print.c

针对加 static 的情况:

$ readelf -s print.o | egrep "hello$|print$"
     6: 00000000     4 OBJECT  LOCAL  DEFAULT    3 hello
     7: 00000000    23 FUNC    LOCAL  DEFAULT    1 print

不加的情况:

$ readelf -s print.o | egrep "hello$|print$"
     9: 00000000     4 OBJECT  GLOBAL DEFAULT    3 hello
    10: 00000000    23 FUNC    GLOBAL DEFAULT    1 print

LOCALGLOBAL 直观地反应了 static 用于限定变量和函数在文件之外是否可访问。加了 static 以后,文件之外不可见。

补充另外一个 nm 工具的结果,针对加 static 的情况:

$ nm print.o | egrep "hello$|print$"
00000000 d hello
00000000 t print

不加的情况:

$ nm print.o | egrep "hello$|print$"
00000000 D hello
00000000 T print

上面四个字母有两组大小写,分别对应 data, text 的 LOCAL 和 GOLOBAL 符号,其中 "hello" 是数据,“print” 作为函数处在代码区域。

man nm: "D" "d" The symbol is in the initialized data section. "T" "t" The symbol is in the text (code) section. If lowercase, the symbol is usually local; if uppercase, the symbol is global (external). There are however a few lowercase symbols that are shown for special global symbols ("u", "v" and "w")

延伸介绍到 nm 这个工具是因为,Linux 内核的 System.map 这样的符号表文件经常会被用来调试,这个文件实际上是用 nm 导出来的。

再延伸一个 WEAK 类型,这个类型类似于不加 static 的 GLOBAL,但是呢,允许定义另外一个同名的函数或者变量,用来覆盖 WEAK 类型的这个:

$ cat print.c
#include <stdio.h>

__attribute__((weak)) char *hello = "hello";

__attribute__((weak)) void print(char *str)
{
  printf("%s\n", str);
}

$ cat hello.c
#include "print.h"

char *hello = "hello, world";

int main(void)
{
  print(hello);
  return 0;
}

$ ./hello
hello, world

这种情况允许某个变量或者函数的“multiple definition”,如果不定义为 WEAK 类型而且不定义为 LOCAL(用 static),这种情况本来是不被允许的:

$ gcc -m32 -o hello x.c print.h print.c
/tmp/ccMO5y0A.o:(.data+0x0): multiple definition of `hello'
/tmp/ccj8KK1s.o:(.data+0x0): first defined here
collect2: error: ld returned 1 exit status

这种用法在内核中被广泛采用,通常用来确保可以添加架构特定的优化函数:

$ grep __weak -ur ./ --include "*.c" | wc -l
413

汇总如下:

关键字

类型

说明

static

LOCAL

限文件内访问

不加 static

GLOBAL

文件外可在 extern 声明后访问

weak attribute

WEAK

同 GLOBAL,但可重定义

从运行的角度

上面从编译和二进制程序文件的角度分析了 static 关键字针对文件层面变量和函数的约定,下面再来看看函数内部的变量,在声明为 static 与否情况下的异同。

作为对比,把其他类型的变量也纳入进来:

$ cat hello.c
#include <stdio.h>

static int m;
static int n = 1000;
int a;
int b = 10000;

static int hello(void)
{
    static int i;
    static int j = 10;
    int x;
    int y = 100;
    register int z = 33;

    printf("i = %d, addr of i = %p\n", i, &i);
    printf("j = %d, addr of j = %p\n", j, &j);
    printf("x = %d, addr of x = %p\n", x, &x);
    printf("y = %d, addr of y = %p\n", y, &y);
    printf("z = %d, in register, no addr\n", z);

    return 0;
}

int main(int argc, char *argv[])
{
    printf("argc = %d, addr of argc = %p\n", argc, &argc);
    printf("argv = %s, addr of argv = %p\n", argv[0], argv);
    printf("m = %d, addr of m = %p\n", m, &m);
    printf("n = %d, addr of n = %p\n", n, &n);
    printf("a = %d, addr of a = %p\n", a, &a);
    printf("b = %d, addr of b = %p\n", b, &b);

    hello();

    return 0;
}

$ gcc -m32 -o hello hello.c
$ ./hello
argc = 1, addr of argc = 0xffd91f60
argv = ./hello, addr of argv = 0xffd91ff4
m = 0, addr of m = 0x804a030
n = 1000, addr of n = 0x804a020
a = 0, addr of a = 0x804a038
b = 10000, addr of b = 0x804a024
i = 0, addr of i = 0x804a034
j = 10, addr of j = 0x804a028
x = -143124200, addr of x = 0xffd91f24
y = 100, addr of y = 0xffd91f28
z = 33, in register, no addr

用二进制程序文件来佐证:

$ readelf -S hello | grep 804a | tail -2
  [24] .data PROGBITS 0804a018 001018 000014 00  WA 0 0 4
  [25] .bss  NOBITS   0804a02c 00102c 000010 00  WA 0 0 4

$ readelf -s hello |  egrep " m$| n$| a$| b$| i| j| x$| y$"
    36: 0804a030     4 OBJECT  LOCAL  DEFAULT   25 m
    37: 0804a020     4 OBJECT  LOCAL  DEFAULT   24 n
    39: 0804a034     4 OBJECT  LOCAL  DEFAULT   25 i.2021
    40: 0804a028     4 OBJECT  LOCAL  DEFAULT   24 j.2022
    54: 0804a024     4 OBJECT  GLOBAL DEFAULT   24 b
    67: 0804a038     4 OBJECT  GLOBAL DEFAULT   25 a

综合上面 3 组数据:

类型

代码

存储区域

文件内

static int m;

.bss(被初始化为 0)

文件内

static int n=1000;

.data

文件内

int a;

.bss(被初始化为 0), GLOBAL

文件内

int b = 10000;

.data, GLOBAL

函数内

static int i;

.bss(被初始化为 0)

函数内

static int j = 10;

.data

函数内

int x;

stack(值随机,未初始化)

函数内

int y = 100;

stack

函数内

register int z = 33;

register(汇编中分配好)

函数参数

argc, argv

stack(默认调用约定)

再补充几点:

1.用 register 定义的变量存放在寄存器中,所以无法获取它们的内存地址(因为根本不存放在内存中)。可以通过查看汇编代码确认:

$ gcc -m32 -S -o hello.s hello.c
$ grep 33 hello.s
  movl  $33, %ebx

2.函数内用 static 定义的变量名(i 和 j)在符号表中都加了后缀,主要是方便多个函数定义同样的变量名,因为这些变量仅限该函数内(含多次调用)可见。

3.函数内非 static 定义的变量,以及函数参数的传递都是通过 Stack 完成的,这些变量只在函数内(包括 Caller, Callee)可见,外部不可见,所以在符号表中也找不到它们。

4.关于函数参数传递,如果明确改变了调用约定,比如函数明确加了 \_\_attribute\_\_((fastcall)) 声明,那么部分参数将通过寄存器传递。不过 main 是例外,因为它的 Caller( __libc_start_main)默认是通过 Stack 传递参数的,再改变它的调用约定就拿不到正确的数据了。

小结

大学的课程蛮多都停留在语法层面的描述,需要透彻理解一些“概念”,还是需要结合实际操作,从终极使用的角度来看这些“概念”,看到的深度和细节会大有不同。

本文分享自微信公众号 - Linux阅码场(LinuxDev),作者:吴章金

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-11-26

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Linux内核同步机制之(一):原子操作

    如果这个操作序列是串行化的操作(在一个thread中串行执行),那么一切OK,然而,世界总是不能如你所愿。在多CPU体系结构中,运行在两个CPU上的两个...

    Linux阅码场
  • 解决Linux内核问题实用技巧之-dev/mem的新玩法

    简单来讲,/dev/mem是系统物理内存的映像文件,这里的 “物理内存” 需要进一步解释。

    Linux阅码场
  • 宋宝华:一个简单的python脚本画出Linux程序/库依赖图

    继《宋宝华:一个简单的python脚本看透Linux程序对库的依赖》之后,作为一个python的初级用户,学习和实践python的步伐根本就不下来!

    Linux阅码场
  • 写了这么久代码,你懂单例模式吗?

    这种方式在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快。这种方式基于类加载机制避免了多线程的同步问题,但是也不能确定有其他的方式(或者其他的静态方...

    咻咻ing
  • 设计模式二十四章经之单例设计模式

    我就是马云飞
  • 设计模式(1)-单例模式

    单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中一个类只有一个实例。即一个类只有一个对象实例。

    秦子帅
  • python购物小票的案例

    ''' 数据: T恤 tshirt 245元 运动鞋 sport 370元 网球拍 tennis 345.5元 指令: 输入:T恤 tshirt 245元 运动...

    py3study
  • 【机器学习】CS229课程笔记notes1翻译-Part II分类和logistic回归

          我们现在谈论分类问题。分类问题与回归问题类似,区别是在分类问题中,我们现在想要预测的y值只取少量的离散值。现在,我们聚焦于二值分类问题,y只取两个值...

    魏晓蕾
  • Python自动化开发学习4-3

    python自带的str()可以完成序列化,然后eval()可以反序列化,但是我们先把他们忘记。不知道适用范围是多大。

    py3study
  • 单例模式 Java 简介 学习笔记 及多种实现方式

    在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个...

    大鹅

扫码关注云+社区

领取腾讯云代金券