Linux内核完全注释之编程语言和环境(二)

时间:2021-08-08 10:30:17

c程序的编译和链接

使用gcc汇编器编译c语言程序时通常会经历四个阶段,即预处理阶段、编译阶段、汇编阶段、链接阶段,如下图。

Linux内核完全注释之编程语言和环境(二)

例如:

gcc -o hello hello.c 生成可执行文件hello

gcc -S -o hello.s hello.c

gcc -c -o hello.o hello.c

嵌入式汇编(内联汇编)

1、格式

asm("汇编语句"

:输出寄存器

:输入寄存器

:会被修改的寄存器)

asm是内联汇编语句的关键词;输出寄存器与输入寄存器都分别对应着一个C变量或常数值,会被修改的寄存器表示你已经对于列出的寄存器中的值进行了修改,gcc编译器不能再依赖于它原先对这些寄存器加载的值。如果必要的话,gcc需要重新加载这些寄存器,因此需要把那些没有在输入输出寄存器部分列出,但是在汇编语句中明确用到或者隐含用到的寄存器名列在这个部分中。

如以下代码的seg代表一指定的内存段值,而addr表示一内存偏移地址量;且该宏函数的作用就是从指定的段和段偏移的内存地址处取出一个字节

#define get_seg_byte(seg,  addr) \
({ \
register char __res; \
__asm__("push %%fs; //保存fs寄存器的原值
mov %%ax, %%fs; //将seg设置到fs
movb %%fs:%, %%a1; //将seg:addr处的一个字节放置到a1寄存器中
pop %%fs "\
: "=a" (__res) \ //输出寄存器列表
:"" (seg), "m"(*(addr)));\ //输入寄存器列表 __res;})

嵌入式汇编程序规定把输出和输入寄存器统一按顺序编号,顺序从输出寄存器序列从左到右从上到下,以%0开始,分别记为%0, %1, ... %9。

输入语句中“0”加载符表示与输出行相同位置相同寄存器

代码 说明 代码 说明
a 使用寄存器eax m 使用内存地址
b 使用寄存器ebx o 使用内存地址并可以加偏移值
c 使用寄存器ecx I 使用常数0-31
d 使用寄存器edx J 使用常数0-63
S 使用寄存器esi K 使用常数0-255
D 使用寄存器edi L 使用常数0-65535
q

使用动态分配字节可寻址寄存器

(eax、ebx、ecx、edx)

M 使用常数0-3
r 使用任意动态分配寄存器 N 使用1字节常数(0-255)
g

使用通用的有效地址即可

(eax、ebx、ecx、edx或内存变量)

O 使用常数0-31
A 使用eax与edx联合(64位) = 输出操作数,输出值将替换当前值
+ 表示操作数可读可写    & 早期会变操作数,表示在使用完操作数之前,内容会被修改

指令leal用于计算有效地址,leal (r1, r2, 4), r3语句表示r1+r2*4 -> r3

在代码执行时,若不希望汇编语句被gcc优化而修改,就需要在asm符号后面添加关键字volatile。其格式 asm valatile (......);  __asm__ __valatile__(......)

关键字valatile 也可以放在函数名前来修饰函数,用来通知gcc该函数不会返回,从而避免gcc对于不返回函数的告警

volatile  void do_exit(long code);

static inline volatile void oom(void)
{
printf("out of memory\n");
do_exit(SIGSEGV);
}

圆括号中的组合语句 ({.......}),且最后一个语句代表表达式的值,若最后一条语句不是表达式,则表示该组合语句具有void类型;实际使用中通常都是用宏来实现

寄存器变量分为全局全局寄存器变量和局部寄存器变量

register int res __asm__("ax")

c与汇编程序相互调用

1、c函数调用机制

Linux内核程序boot/head.s执行完基本初始化后,就会跳转到去执行init/main.c程序,那么head.s程序是如何把执行控制权交给main.c的?

c函数的调用机制和控制权转移

函数调用包括从一块代码到另一块代码之间的双向数据传递和执行控制权转移;数据传递通过函数参数和返回值来进行,另外还包括局部变量的申请与释放。数据传递与局部变量 存储空间的回收和分配都是通过栈操作来实现的。

2、栈帧结构和控制权转移方式

栈被用来传递函数参数、存储返回信息、临时保存寄存器原有值以备恢复、存储局部变量。单个函数调用操作所使用的栈部分被称为栈帧(stack frame)结构。帧结构的两端由两个指针来指定,寄存器ebp通常用作帧指针,而esp则用作栈指针,在函数执行的过程中,由于栈指针会随着数据的入栈与出栈而移动,故函数中对大部分数据的访问都是基于帧指针而进行。

Linux内核完全注释之编程语言和环境(二)

指令CALL和RET用于处理函数调用和返回操作,调用指令CALL的作用是把返回地址压入栈中,并且跳转到被调用函数开始处执行。返回地址是程序中紧随调用指令CALL后面一条指令的地址,因此当调用函数返回时,就会从该位置继续执行。

返回指定RET用于弹出栈顶处的地址并跳转到该地址处,使用该指令前,应该先正确处理栈中内容,使得当前栈指针所指位置内容正是先前CALL指令保存的返回地址

3、Intel CPU采用了所有函数必须遵循的寄存器用法统一惯例:

寄存器eax,ecx,edx的内容必须由调用者自己负责保存,也就是被调用函数B可以不管调用函数A是否使用而随意使用

寄存器ebx,esi,edi,ebp,esp的内容必须由被调用者来保护,当被调用者B需要使用此中某寄存器时,需要先将寄存器中的当前值保存到栈中,并在退出时恢复其内容

汇编调用c程序

linux0.11目标文件格式

System.map文件

1、当运行GNU链接器gld(ld)时,若使用了“-M”,或者使用nm命令,则会在标准输出设备(通常是屏幕)上打印出链接映像(link map)信息,即是指由链接程序产生的目标程序内存地址映像信息,其中列出了程序装入到内存中的位置信息。具体来讲有如下信息:

  • 目标文件及符号映射到内存中的位置
  • 公共符号如何放置
  • 链接中包含的所有文件成员以及引用的符号

2、通常我们会把送到标准输出设备的链接映像信息重定向到一个文件中(例如system.map)。符号表是所有内核的符号及其对应地址的一个列表(包括上文提到的end,etext, edata等符号的地址信息)。

3、每次内核编译都会产生一个新的对应system.map文件,当内核运行出错时,通过system.map文件中的符号表解析,就可以查到一个地址值对应的变量名,或反之。

4、符号表示例

Linux内核完全注释之编程语言和环境(二)

第一列表示符号值(地址);第二栏是符号类型,指明符号位于目标文件的哪个区或其属性;第三栏是对应的符号名称。

符号类型    名称                       说明
A Absolute 符号值是绝对值,并且在进一步链接过程中不会改变
B BSS 符号在bss区中,即bss段
C Common

符号是公共的,公共符号是未初始化数据,在链接时,多个公共符号可能具有同一名称。

如果该符号定义在其他地方,则公共符号被看作是未定义的引用

D Data 符号在已初始化的数据中
G Global

符号是在小对象已初始化数据区中的符号。某些目标文件的格式允许对小数据对象(例如一个

全局整型变量)可进行更有效的访问

I Indirect 符号是对另一个符号的间接引用
N Debugging 符号是一个调试符号
R readonly 符号在一个只读数据区中
 S  Small  符号是小对象未初始化数据区中的符号
 T  Text  符号是代码区中的符号
 U  Undefined  符号是外部符号,并且值为0(未定义)
 -  stabs  符号是a.out目标文件中的一个stab符号,用于保存调试信息
 ?  unknown  符号的类型未知,或者是与具体文件格式有关