学习笔记分享

分享与展示个人专业知识学习笔记

0%

在C语言中想在函数参数列表中传入不固定数量和类型的参数,可以使用stdarg.h头文件,该头文件定义了一系列宏用以实现可变参数列表。

实际上,最熟悉的printfscanf函数就是这么实现的。

用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdarg.h>					// 包含该头文件

int sum(int n_elements, ...) // 给出一个命名(有名称的)参数,一串省略号
{
int sum = 0;

va_list var_arg; // 声明一个va_list类型变量

va_start(var_arg, n_elements); // 初始化该变量

for (int i=0;i<n_elements;i++)
{
sum += va_arg(var_arg, int);// 访问参数
}

va_end(var_arg); // 结束访问

return sum;
}
  1. 在函数参数列表中,提供一或多个有名称的参数,最后跟上...提示此处传递数量和类型未知的参数。
  2. va_list类型声明一个变量用于访问可变参数列表。
  3. va_start()接受一个va_list变量和最后一个有名称参数用于初始化,将该va_list变量设置为指向第一个可变参数列表的参数(由给定的最后一个有名称的参数通过相对偏移来确定位置)。
  4. va_arg()接受一个va_list变量和类型名称,按类型返回当前位置参数的值,并将va_list变量向后移动。
  5. va_end()用于将va_list置空,并完成一些清理工作。

注意

  1. 可变参数传递过程中会发生默认参数提升,也就是说没有char/short/float型,因为它们已经被提升了。(详见默认参数提升或见**参考**部分相关文章)现在编译器很智能,当你试图使用这些类型访问时会给出警告:使用char/short/float访问va_arg
  2. va_end()是必须的。有人认为va_end()是不必要的,因为其编译环境下该函数只作了对va_list置空操作。但不同编译环境下,有可能va_start()可能以某种方式修改了堆栈,这种修改可能导致返回无法完成va_end()会将其复原;当然也可能有其他实现。因此,基于可移植性的考虑,va_end当然是必须的。

参考

  1. 《Pointers On C》
  2. 可变长参数列表误区与陷阱——va_arg不可接受的类型
  3. 可变长参数列表误区与陷阱——va_end是必须的吗?

行为

在参数传递过程中,

charshort被提升为int/ unsigned int

float被提升为double

发生情形

在参数的预期类型未知时,发生默认参数提升

也就是说,当没有原型参数是可变参数时。

背景/产生原因

在 1988 年之前的黑暗时代,经典的“K&R”C 中没有函数原型之类的东西,并且设置了默认参数提升,因为:

  • (a) 本质上“免费”,因为将一个字节放入寄存器并不比将一个字放入寄存器中花费更多。
  • (b)减少参数传递中的潜在错误。第二个原因从来没有完全解决它,这就是为什么在 ANSI C 中引入函数原型是 C 语言中最重要的变化。

参考

什么是链接属性

我们编译C程序时通常不止一个源文件,不同源文件中可能有相同的标识符,链接就决定了不同源文件中标识符的关系,即相同标识符是否指向同一个实体。

更通俗地说,就是在两个不同文件中的变量、函数声明是否指向同一个实体。比如:a、b文件同时声明了变量c,链接属性就指定了这两处变量c是否是同一个c。

链接属性指明了这一关系。通过改变标识符链接属性,你能决定在a文件中的标识符要不要指向b文件中相同标识符的声明。

链接属性的分类

链接属性有三种:

  • external - 外部链接
  • internal - 内部链接
  • none - 无链接

对于external属性的标识符,不同文件中出现的多个同名称标识符指向同一个实体。在C语言中,用extern关键字在声明中指定以引用其他文件中定义的相同标识符

对于internal属性的标识符,仅在当前文件内该标识符指向同一个实体。在C语言中,用static关键字在声明中指定让标识符变为该文件私有(只有对原本缺省的链接属性为external的标识符,才能用static关键字改变其链接属性为internal)。

对于none属性的标识符,在每个声明位置都是一个新的实体。C语言中,没有对应的关键字,由上下文确定。

默认的链接属性

标识符的默认的链接属性与其出现的位置有关。

  • 程序的全局变量、所有函数默认的链接属性为external。

  • 其余标识符的默认链接属性为none。

在以下例子中,b、c、f的链接属性就是external:

1
2
3
4
5
6
7
typedef char *a;
int b;
int c(int d)
{
int e;
int f(int g);
}

具体用法

extern

在a文件中想要使用b文件中定义的external属性标识符,可使用extern关键字在a文件中声明。

即使该标识符所在上下文下默认链接属性为external,也建议使用extern关键字显式说明,有利于增加程序可读性

static

在a文件定义了一个全局标识符,但不想被其他文件访问,可以对该标识符加上static关键字。

在a、b文件中定义了同样的标识符,通过static关键字可以避免多重定义问题。

同一个标识符在链接中只能存在一个,那么通过内部链接的方式可以隔绝同名外部链接,且限定了外部编译单元不能访问该文件全局标识符。

再次提醒:只有对原本缺省的链接属性为external的标识符,才能用static关键字改变其链接属性为internal

一些细节

  1. 对于external属性的标识符,你可以在多个不同源文件中声明,但是你只能在一处初始化。否则就会出现重复定义的问题:multiple definition of 'a';
  2. extern关键字声明的标识符用于访问其他文件中定义的同名的标识符,因此无法进行初始化。如果你对extern声明的变量进行初始化就会生成警告:warning: ‘a’ initialized and declared ‘extern’
  3. 如果在其他文件中不存在相应的标识符定义,却在当前文件中使用了extern声明,会报错:undefined reference to 'a',原理同2。
  4. external属性的标识符总是静态存储类型。
  5. static关键字还有改变存储类型的作用,因此,其作用与上下文环境有关,只有对于默认链接属性为external的标识符,才有改变链接属性的作用。
  6. C++中,const变量隐含的具有internal属性,C中并不具有这一性质。

实例分析

external链接重复定义

a.c:

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int a = 3;

int main(void)
{
printf("a = %d\n", a);
return 0;
}

b.c:

1
int a = 1;

编译运行:

1
2
3
$ gcc a.c b.c
/usr/bin/ld: /tmp/ccoNPRr3.o:(.data+0x0): multiple definition of `a'; /tmp/cc8qHzR5.o:(.data+0x0): first defined here
collect2: error: ld returned 1 exit status

a.c和b.c中的变量a在上下文中缺省为外部链接属性,两处都进行了声明,因此报错重复定义。

internal和none

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static int i; // definition
// static storage
// internal linkage

void f(void)
{
extern int i; // declaration
// refers to the static i at file scope
// note that even though the specifier is extern
// its linkage is intern (this is legal in both C/C++)
{
int i; // definition
// automatic storage
// no linkage
}
}
  1. 作用域不同。这个跟链接属性无关,不是由链接属性决定的,作用域只与其上下文位置有关。但不难看出,internal属性标识符出现的位置都是在文件作用域,而none往往在代码块作用域。
  2. 参不参与链接。这段代码很好地说明了internal属性的变量在文件内部参与链接,因为你能够在内部通过extern声明显示得到。而没有链接属性的变量无法通过这一方式得到。

internal链接

a.c:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

static int a = 3;

int main(void)
{
extern int a;
printf("a = %d\n", a);
return 0;
}

b.c:

1
int a = 1;

编译运行:

1
2
3
$ gcc a.c b.c
$ ./a.out
a = 3

参考

一般(非指针)变量

对于一般类型(指非指针)变量,const关键字表明该变量为常量,其值不能被修改,声明一个常量和声明一个变量的方法类似:

1
2
const int a;
int const a;

这两种方法等价。

指针变量

对于指针变量,const可能修饰的对象就有指针和指针指向的实体两种了。

1
2
3
const int *pci;
int const *pic;
int * const cpi;

第一种和第二种修饰的是指针指向的实体,第三种则修饰指针。

简单小结一下,就是如果const*(解引用/间接)运算符前,修饰的就是实体;在*后则修饰指针。

多个变量

考虑更复杂的情况,当同时存在多个变量时,const对后续变量是否有效?

利用下述测试代码,通过对const限定符的变量进行修改,可以得到编译器的错误信息,从而得知const的作用范围:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main(void)
{
const int a=1, b=2;
a = b;
b = a;

int const c=3, d=4;
c = d;
d = c;

int e=5,f=6;
int *const pe=&e, *pf=&f;
pe = pf;
pf = pe;

const int *pe2=&e, *pf2=&f;
*pe2 = f;
*pf2 = e;

return 0;
}

result

可见,只有单独对指针进行修饰的const关键字是只作用于单个变量的,其余都是对后续声明生效的。

在C语言中,字符串常量的直接值,实际上是一个指针,而不是这些字符本身。

可以这么理解,C语言中的字符串是一串以'\0'结尾的字符组成的数组。而数组和指针某种意义上是等价的。

也就是说,字符串常量"abcd"在表达式中实际上代表的值是该字符串第一个字符的地址。这同时也解释了为什么字符串常量可以直接赋给指针,而不能赋给数组。

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>

int main(void)
{
/* 这里通过人为的错误类型赋值,得到警告信息 */
int a = "abcd";

/* 对"abcd"与指针类型进行对比操作 */
char *ptr = "abcd";

printf("ptr: %x\n", ptr);
printf("\"abcd\": %x\n", "abcd");

printf("ptr+1: %x\n", ptr + 1);
printf("\"abcd\"+1: %x\n", "abcd" + 1);

printf("*\"abcd\"=%x\n", *"abcd");
printf("*(\"abcd\"+1)=%x\n", *("abcd"+1));

if (ptr == "abcd")
{
printf("ptr==\"abcd\"\n");
}

return 0;
}

得到的警告信息说明,字符串常量实际是char *类型:

warning

输出结果如下:

result

测试环境

  • windows 10
  • ubtuntu 20.04

操作方法

Windows

新起一行输入Ctrl-Z,然后回车

Linux

法一

新起一行输入Ctrl-D

法二

同一行中,连续输入两次Ctrl-D

引言

在C语言中,为什么要用预处理指令而不是直接用注释来去除代码?

考虑代码中可能原先就存在注释的情况,直接在首尾加上/**/就可能存在前后注释符号匹配错误问题。

实现

1
2
3
#if 0
statements
#endif

这样,即使原先存在注释,也能有效”注释掉“该段代码了。

Note: 在#if#endif指令之间的代码,只有当#if后的条件成立时才会被编译

问题

用pip安装包时警告:

1
2
WARNING: The script script_name is installed in '/home/user_name/.local/bin' which is not on PATH.
Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.

意思是你安装的路径'/home/user_name/.local/bin'不是PATH路径

这样的话你就不能在命令行里直接使用库相关的命令

例如我刚安装了pwntools库,如果我安装在PATH路径下,我就可以直接在命令行输入pwn来执行相关命令,而不是通过python -m pwn的方式执行相关命令

解决

给出两个消除警告的方法,请根据需要选择

法一:添加库到PATH路径

这样配置的作用在于你可以把安装的库作为独立的命令执行,而不是通过python -m的方式

使用以下命令在~/.bashrc文件后追加export PATH="$PATH:$HOME/.local/bin"这一行

1
echo 'export PATH="$PATH:$HOME/.local/bin"' >> ~/.bashrc

然后用以下命令使刚才的配置生效

1
source ~/.bashrc

法二:忽略警告

按照提示,如果您希望禁止显示此警告,请使用 --no-warn-script-location

报错原因

使用了-m32指令,而64位系统GCC默认安装环境无法直接编译生成32位程序

解决方案

安装Multilib,顾名思义,就是多重的。用它可以替代原来单一的lib,这样就既能产生32位的代码,又能生成64位的代码。

1
apt-get install gcc-multilib

参考

学Pwn的时候要用到checksec工具,用于查看程序开启了哪些保护机制

Step1-git下载

1
git clone https://github.com/slimm609/checksec.sh.git

Step2-建立符号链接

1
sudo ln -s ~/checksec.sh/checksec /usr/local/bin/checksec

Step3-使用

详见https://github.com/slimm609/checksec.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$ checksec
Usage: checksec [--format={cli,csv,xml,json}] [OPTION]


Options:

## Checksec Options
--file={file}
--dir={directory}
--listfile={text file with one file per line}
--proc={process name}
--proc-all
--proc-libs={process ID}
--kernel[=kconfig]
--fortify-file={executable-file}
--fortify-proc={process ID}
--version
--help
--update or --upgrade

## Modifiers
--debug
--verbose
--format={cli,csv,xml,json}
--output={cli,csv,xml,json}
--extended

For more information, see:
http://github.com/slimm609/checksec.sh