最近感觉自己的知识漏洞太多了,必须寻找正确的学习方法。知识在于积累,一天不积累就相当于退步。
正确的学习方法很重要,一定要打牢基础知识。__attribute__这个词,可以说两年前我就看过,忘了看了多少次了。可是对他的使用仍然是一知半解。这说明我之前的学习方法不对,没认识到积累知识的重要性。罪过!
本文参考了博客:http://blog.csdn.net/juana1/article/details/6849120
GNU C的一大特色(却不被初学者所知)就是__attribute__机制。__attribute__可以设置函数属性(Function Attribute)、变量属性(Variable Attribute)和类型属性(Type Attribute)。它的书写特征是:__attribute__前后都有两个下划线,并切后面会紧跟一对原括弧,括弧里面是相应的__attribute__参数,语法格式如下:
__attribute__ ((attribute-list))
另外,它必须放于声明的尾部“;”之前。
函数属性
常见的函数属性有返回值,参数。相应的用法有:
__attribute__((noreturn));//通知编译器该函数没有返回值。
format (archetype, string-index, first-to-check);//通知编译器函数参数列表信息。
__attribute__(const);//打开-O选项,编译器发现后,可以减少调用次数。
__attribute__((__no_instrument_function__));//通常用于追踪函数调用关系。
函数属性可以帮助开发者把一些特性添加到函数声明中,从而可以使编译器在错误检查方面的功能更强大。__attribute__机制也很容易同非GNU应用程序做到兼容之功效。
GNU CC需要使用 –Wall编译器来击活该功能,这是控制警告信息的一个很好的方式。下面介绍几个常见的属性参数。
__attribute__ format。该__attribute__属性可以给被声明的函数加上类似printf或者scanf的特征,它可以使编译器检查函数声明和函数实际调用参数之间的格式化字符串是否匹配。该功能十分有用,尤其是处理一些很难发现的bug。format的语法格式为:
format (archetype, string-index, first-to-check)
format属性告诉编译器,按照printf, scanf, strftime或strfmon的参数表格式规则对该函数的参数进行检查。“archetype”指定是哪种风格;“string-index”指定传入函数的第几个参数是格式化字符串;“first-to-check”指定从函数的第几个参数开始按上述规则进行检查。
具体使用格式如下:
__attribute__((format(printf,m,n)))
__attribute__((format(scanf,m,n)))
其中参数m与n的含义为:
m:第几个参数为格式化字符串(format string);
n:参数集合中的第一个,即参数“…”里的第一个参数在函数参数总数排在第几。比如说,
//extern void myprint(const char *format,int i,...) __attribute__((format(printf,1,3)));
表示第一个参数的格式化控制字符串,第三个参数是...参数的开始。可见,__attribute__对移植很有用。
注意,有时函数参数里还有“隐身”的呢,后面会提到;
A://extern void myprint(const char *format,int i,...) __attribute__((format(printf,1,3))); 或者是 extern void myprint(const char *format,int i,...) __attribute__((__format__(printf,1,3)));
B:extern void myprint(const char *format,...);// __attribute__((format(printf,1,2)));
void test()
{
myprint("i=%d\n",66,6);
myprint("i=%s\n",66,"abc");
myprint("%s,%d,%d\n",66,"1","2",5);
}
在编译的时候,加上-Wall,开启所有警告信息,发现,使用 A时有警告,B则没有警告。说明加上attribute后,可以帮我们查看使用函数的合法性很有用。
还有一个__attribute__ noreturn,该属性通知编译器函数从不返回值,当遇到类似函数需要返回值而却不可能运行到返回值处就已经退出来的情况,该属性可以避免出现错误信息。C库函数中的abort()和exit()的声明格式就采用了这种格式,如下所示:
extern void exit(int) __attribute__((noreturn));
extern void abort(void) __attribute__((noreturn));
为了方便理解,大家可以参考如下的例子:
extern int ret(int x) __attribute__((noreturn));int ret(int x){
return;
}
以上代码编译的时候会有警告:function declared ‘noreturn’ has a ‘return’ statement [enabled by default];
若将 extern int ret(int x) __attribute__((noreturn));改为 extern int ret(int x);则不会有警告。
或者将return 改为exit(1);则也不会有警告。
这样做可以限制一个不存在返回值的函数的,却返回了一个值,编译器会发出警告。
__attribute__ const
该 属性只能用于带有数值类型参数的函数上。当重复调用带有数值参数的函数时,由于返回值是相同的,所以此时编译器可以进行优化处理,除第一次需要运算外,其 它只需要返回第一次的结果就可以了,进而可以提高效率。该属性主要适用于没有静态状态(static state)和副作用的一些函数,并且返回值仅仅依赖输入的参数。在函数体内,不要引起副作用。比如下面这个函数是不适用于此方法的。int func(int x){static y=0;y++;return x;}。下面举一个正确的例子说明一下:(要想看到效果,必须使用编译器优化处理)
#include <stdio.h>
int
__attribute__((
const
)) inc(
int
x)
{
printf
(
"%s(%d)\n"
, __FUNCTION__, x);
return
x + 1;
}
int
inc2(
int
x)
{
printf
(
"%s(%d)\n"
, __FUNCTION__, x);
return
x + 1;
}
int
main(
void
)
{
int
i, j;
i = inc(10);
j = inc(10);
printf
(
"%d %d\n"
, i, j);
i = inc2(10);
j = inc2(10);
printf
(
"%d %d\n"
, i, j);
return
0;
}
用gcc -O -o const const.c后运行const, inc(10)只会输出一次。
编译成汇编文件看看。gcc -S -O 生成youhuaconst.s
gcc -S 生成const.s
diff youhuaconst.s const.s | grep inc
> call inc
可以看出不加-O的const.s,多了一次call inc.
需要注意的是,attibute((const))修饰的函数声明,而不是函数定义。
像下面这个写法就是错误用法,
int
inc(
int
x)
{
printf
(
"%s(%d)\n"
, __FUNCTION__, x);
return
x + 1;
} __attribute__((
const
));编译的时候警告:warning: empty declaration [enabled by default]
编译链接后允许,发现inc还是调用了2次。必须在声明的时候加以限制。
__attribute__((__no_instrument_function__))
-finstrument-functions 与 __no_instrument_function__
在追踪函数调用关系的时候,可以给gcc传-finstrument-functions的参数(不能与-O选项同时使用)。然后实现
void __cyg_profile_func_enter(void *this_func, void *call_site)和
void __cyg_profile_func_exit(void *this_func, void *call_site)。
需要注意的是,声明这两个函数的时候,必须加上__attribute__((no_instrument_function));说明,否则会出现函数调用死循环。
下面的例子可以打印函数地址和调用者地址。从而可以查看函数调用关系。
#define DUMP(func, call) \
printf("%s: func = %p, called by = %p\n", __FUNCTION__, func, call)
void __attribute__((__no_instrument_function__))
__cyg_profile_func_enter(void *this_func, void *call_site)
{
DUMP(this_func, call_site);
}
void __attribute__((__no_instrument_function__))
__cyg_profile_func_exit(void *this_func, void *call_site)
{
DUMP(this_func, call_site);
}
int cc(int c)
{
return c+c;
}
int bb(int b)
{
return b*b;
}
int aa(int a)
{
return a+1;
}
main()
{
printf("The %d \n", aa(bb(cc(2))));
}
编译的时候:
root@ubuntu:~/Desktop/proctice/gnu# gcc -finstrument-functions a.c -oa
root@ubuntu:~/Desktop/proctice/gnu# ./a
__cyg_profile_func_enter: func = 0x8048574, called by = 0xb75f94d3
__cyg_profile_func_enter: func = 0x80484c0, called by = 0x804859c
__cyg_profile_func_exit: func = 0x80484c0, called by = 0x804859c
__cyg_profile_func_enter: func = 0x80484fb, called by = 0x80485a4
__cyg_profile_func_exit: func = 0x80484fb, called by = 0x80485a4
__cyg_profile_func_enter: func = 0x8048539, called by = 0x80485ac
__cyg_profile_func_exit: func = 0x8048539, called by = 0x80485ac
The 17
__cyg_profile_func_exit: func = 0x8048574, called by = 0xb75f94d3
然后把a文件进行反汇编,objdump -d -S a > a.s
在反汇编文件a.s中,可以看到函数---地址的关系。如:
root@ubuntu:~/Desktop/proctice/gnu# cat a.s | grep 574
8048397: 68 74 85 04 08 push $0x8048574
08048574 <main>://说明main的地址是0x8048574。
那么func = 0x8048574, called by = 0xb75f94d3,意思就是地址为0xb75f94d3的函数调用了main函数,其实地址0xb75f94d3为shell的地址。
root@ubuntu:~/Desktop/proctice/gnu# cat a.s | grep 80484c0
080484c0 <cc>://函数cc的地址。
那么func = 0x80484c0, called by = 0x804859c意思就是,函数cc被地址0x804859c调用。0x804859c是谁呢?
看了反汇编文件:
8048590: c7 04 24 02 00 00 00 movl $0x2,(%esp) //cc的参数入栈.在多参数的函数中,入栈顺序是从右到左。
8048597: e8 24 ff ff ff call 80484c0 <cc>。 //调用cc函数。
804859c: 89 04 24 mov %eax,(%esp) //这个地址也是cc的返回地址。在main.c中。
也可以在编译的时候加上-g选项生成符号信息,使用addr2line来查看各地址对应的代码。
gcc -finstrument-functions -g a.c -oa
root@ubuntu:~/Desktop/proctice/gnu# addr2line -a 0x80484c0 -e a
0x080484c0
/root/Desktop/proctice/gnu/a.c:17
通过查看a.c的17行,正好是cc函数的{。
16 int cc(int c)
17 {
18 return c+c;
19 }
使用addr2line的缺点在于必须加上-g选项,使用finstrument-functions的缺点是不能与-O选项一起用。
另外-g在使用的时候,也不能与-O并用。否则可能会出现一些比较奇怪的问题。入:
root@ubuntu:~/Desktop/proctice/gnu# gcc -O -g a.c -oa
这个时候再反汇编看一下:
root@ubuntu:~/Desktop/proctice/gnu# objdump -S -d a > oa.s
root@ubuntu:~/Desktop/proctice/gnu# cat oa.s | grep cc
80483cc: 66 90 xchg %ax,%ax
080484f2 <cc>:
然后使用addr2line.
root@ubuntu:~/Desktop/proctice/gnu# addr2line -a080484f2-e a
0x080484c0
/root/Desktop/proctice/gnu/a.c:18
查看a.c的第18行
16 int cc(int c)
17 {
18 return c+c;
19 }
可以看出,cc的首地址变成了return c+c,而不是第一个{,也许是被优化掉了。
也可以同时使用多个属性,多个attribute控制。如:
extern void func(const char *format,...) __attribute__((noreturn)) __attribute__((format(printf,1,2));
或者合并成extern void func(const char *format,...) __attribute__((noreturn,format(printf,1,2)));
增加段(add segment):
用法:__attribute__((section(section_name)))
用objdump -h 可以看一个ELF文件的段属性。
objdump -h a.o:
Idx Name Size VMA LMA File off Algn
...
.text 00000083 00000000 00000000 00000034 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
...
这里面的LMA VMA都是0,是因为还没有进行链接。在链接的时候会设置段的地址。
ld -m elf_i386 --hash-style=gnu -dynamic-linker /lib/ld-linux.so.2 -z relro a.o -o a /usr/lib/gcc/i686-linux-gnu/4.7/../../../../lib/../lib/i386-linux-gnu/crt1.o /usr/lib/gcc/i686-linux-gnu/4.7/../../../../lib/../lib/i386-linux-gnu/crti.o /usr/lib/gcc/i686-linux-gnu/4.7/crtbegin.o -lc /usr/lib/gcc/i686-linux-gnu/4.7/crtend.o /usr/lib/gcc/i686-linux-gnu/4.7/../../../../lib/../lib/i386-linux-gnu/crtn.o -T $1,其中$1是ld --verbose生成的def.lds。
objdump -h a:
Idx Name Size VMA LMA File off Algn
...
.text 000001f8 080482a0 080482a0 000002a0 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
...
可以看到a的text段 VMA和LMA地址是080482a0 。
下面举例说明,如何自定义段。
第一步,使用 __attribute__((section(sectioname))).增加段。
第二步:修改链接文件,x.lds,在lds文件中增加相应的段属性。
第三步:重新编译,链接文件使用修改后的x.lds.
如:
c代码:
char __attribute__((section("myprivsection"))) prd[]= "abcdefghijklm";
int y=0x67;
extern int y;
my.lds增加: .myprisection :
{
/*0804a020*/
. = ALIGN(4);
y = . + 4 ;
y = . + 4 ;
y = . + 4 ;/*这里的y是指 全局变量 y的地址*/
*(myprivsection)
. = ALIGN(4);
}
使用my.lds链接后的a.o生成了a。运行a发现y的值已经有0x67变成了0x68676665(efgh的ASCALL码,小端模式)。
需要注意的是,y = .+ 4 必须放在 *(myprivsection)的前面,这跟lds文件解析有关系。无论执行几次y = .+ 4 效果都是一样的,且相当于y的地址变成了prd+4了。打印出来看一下:
prd is 0x8048498,&y is 0x804849c
root@ubuntu:~/Desktop/proctice/lds# objdump -h a | grep mypr
12 .myprisection 00000010 08048498 08048498 00000498 2**0
可以看出段myprisection 的地址08048498 ,即为prd数组的首地址,而y的地址,就是08048498 +4.
变量属性(Variable Attribute)
关键字__attribute__也可以对变量或结构体成员进行属性设置。设置方法,通常是变量 __attribute__((aligned(num))) [=赋值] ;或者 __attribute__((aligned))
或者 __attribute__((packed))。其中num是字节个数,这个num的值必须是2的n次方。
如果不加num,那么编译器将依据你的目标机器情况使用最大最有益的对齐方式,这种对齐方式有利于内存拷贝的速度。packed则是以最节省内存的方式对齐即1字节对齐。
如:对变量进行属性设置: short t __attribute__((aligned(32)))=0;//在网上看的有的说这时候t就占了32个字节。但是用gcc编译后,打印出来sizeof(t)仍然是2,等于sizeof(short);
但是,如果t是一个结构体的成员的话,将会按照aligned 的设置去对齐,如:
struct T { short t __attribute__ ((aligned (512*16))); };//sizeof(struct T)=8192。
structT{ int t __attribute__ ((aligned (2))); };//sizeof(struct T)=4。aligned设置的值如果不足以存放成员变量时,则按照成员变量的大小去存储。
需要注意的是,attribute属性的效力与你的连接器也有关,如果你的连接器最大只支持16字节对齐,那么你此时定义32字节对齐也是无济于事的。
类型属性(Type Attribute)
关键字__attribute__也可以对结构体(struct)或共用体(union)进行属性设置。大致有六个参数值可以被设定,即:aligned, packed, transparent_union, unused, deprecated 和 may_alias首先来看一下,系统默认的最适合的对齐方式:
typedef struct p3{
char a;//xxxa
}__attribute__((aligned)) p3_t;//sizeof(p3_t)=16.说明系统最适的对齐方式是16字节。
struct S { short f[3]; } ;//sizeof(struct S)=6
struct S { short f[3]; } __attribute__ ((aligned (8)))//sizeof(struct S)=8struct foo { short f[3]; } __attribute__ ((aligned));)//sizeof(struct S)=16,在x86下运行的结果。
下面的例子中,my-packed-struct类型的变量数组中的值将会紧紧的靠在一起,但内部的成员变量s不会被“pack”,如果希望内部的成员变量也被packed的话,my-unpacked-struct也需要使用packed进行相应的约束。
struct my_unpacked_struct
{
char c;
int i;
};
struct my_packed_struct
{
char c;
int i;
struct my_unpacked_struct s;
//struct my_unpacked_struct s __attribute__((packed));//与上面的相同。
}__attribute__ ((__packed__));//sizeof(struct my_packed_struct) =13.这里的packed的影响不到struct my_unpacked_struct。
变量属性与类型属性举例
下面的例子中使用__attribute__属性定义了一些结构体及其变量,并给出了输出结果和对结果的分析。
- // 程序代码为:
- struct p
- {
- int a;
- char b;
- char c;
- }__attribute__((aligned(4))) pp;
- struct q
- {
- int a;
- char b;
- struct n qn; //8字节
- char c;
- }__attribute__((aligned(8))) qq;
- int main()
- {
- printf("sizeof(int)=%d,sizeof(short)=%d.sizeof(char)=%d\n",sizeof(int),sizeof(short),sizeof(char));
- printf("pp=%d,qq=%d \n", sizeof(pp),sizeof(qq));
- return 0;
- }
- //输出结果:
- sizeof(int)=4,sizeof(short)=2.sizeof(char)=1
- pp=8,qq=24
分析:
sizeof(pp):
sizeof(a)+ sizeof(b)+ sizeof(c)=4+1+1=6<23=8= sizeof(pp)
sizeof(qq):
sizeof(a)+ sizeof(b)=4+1=5
sizeof(qn)=8;即qn是采用8字节对齐的,所以要在a,b后面添3个空余字节,然后才能存储qn,
4+1+(3)+8+1=17
因为qq采用的对齐是8字节对齐,所以qq的大小必定是8的整数倍,即qq的大小是一个比17大又是8的倍数的一个最小值,由此得到
17<24+8=24= sizeof(qq)。如果不加__attribute__((aligned(8)))则为20字节。typedef struct p1{
char a;//xa
short b;//bb
int c;//cccc
char d;//d
char e;//e,到此为止10个字节,最大成员变量占4字节,大于10能被4整除的最小的数是12.
}p1_t; //sizeof(p1_t)=12.
typedef struct p2{
char a;//1
short b;//2
int c;//4
char d;//1
char e;//1
}__attribute__((packed)) p2_t; //sizeof(p2)=9.
typedef struct p5{
38 char a;
39 short b;
40 int c;
41 char d;
42 char e;
43 }__attribute__((aligned(128*8*1024))) p5_t;//sizeof(p5_t)=1048576=128*8*1024。
typedef struct p3{
char a;//xxxa
short b;//xxbb
int c;//cccc
char d;//xxxd
char e;
}__attribute__((aligned)) p3_t;//不加attribute大小为12.但是因为系统最适对齐方式是16字节。大于12且是16的整数倍的最小数就是16.
还有一种比较奇怪的现象。
typedef struct p2{
char a;//1
short b;//2
int c;//4
char d;//1
char e __attribute__((aligned(16)));
}__attribute__((packed)) p2_t;//sizeof(p2_t)=32.
struct my_packed_struct
{
char c;
int i;
struct my_unpacked_struct s __attribute__((aligned(128)));
}__attribute__ ((__packed__));//sizoef(struct my_packed_struct) 是128*2 即 256.大概是 __attribute__((aligned(128)));覆盖了__attribute__ ((__packed__))的原因吧。
typedef struct p1{
char a;//xa
short b;//bb
//**** //4,为什么这四个字节没有被填空。
unsigned long long c;//ccccccc,换成double效果一样。
char d;//dxxx
char e;//10
}p1_t;//sizeof(p1_t)=16,为什么不是20?
sizoef(unsigned long long)=8,为什么c的开始位置不是8的整数倍呢?而是按照4的整数倍计算呢?