文章目录
- 一. 学习并掌握可执行程序的编译、组装过程。学习任务:
- 1)阅读、理解和学习材料“用gcc生成静态库和动态库.pdf”和“静态库.a与.so库文件的生成与使用.pdf”,请在Linux系统(Ubuntu)下如实仿做一遍。
- 2)在第一次作业的程序代码基础进行改编,除了x2x函数之外,再扩展写一个x2y函数(功能自定),main函数代码将调用x2x和x2y ;将这3个函数分别写成单独的3个 .c文件,并用gcc分别编译为3个.o 目标文件;将x2x、x2y目标文件用 ar工具生成1个 .a 静态库文件, 然后用 gcc将 main函数的目标文件与此静态库文件进行链接,生成最终的可执行程序,记录文件的大小。
- 3)将x2x、x2y目标文件用 ar工具生成1个 .so 动态库文件, 然后用 gcc将 main函数的目标文件与此动态库文件进行链接,生成最终的可执行程序,记录文件的大小,并与之前做对比。
- 二. Gcc不是一个人在战斗。
- 三. 编写一个C程序,重温全局常量、全局变量、局部变量、静态变量、堆、栈等概念
一. 学习并掌握可执行程序的编译、组装过程。学习任务:
1)阅读、理解和学习材料“用gcc生成静态库和动态库.pdf”和“静态库.a与.so库文件的生成与使用.pdf”,请在Linux系统(Ubuntu)下如实仿做一遍。
用GCC生成.a静态库和.so动态库:
第一步:编辑生成例子程序hello.h、hello.c和main.c
首先创建一个作业目录,以此保存本次练习的文件:
#mkdir test1
#cd test1
使用 ls 命令来列出当前目录中的文件和目录。
使用pwd命令显示当前工作目录的完整路径
然后我使用vim 生成三个子程序文件:
vim hello.h
vim hello.c
vim main.c
vim hello.h:
vim hello.c:
vim main.c:
第二步:将hello.c编译成.o文件:
使用gcc命令得到hello.o文件并使用ls命令查看是否生成了hello.o文件:
gcc -c hello.c
ls
可以看到我们的hello.o 文件,操作完成
第三步:由.o文件创建静态库:
静态库的文件名命名规则:lib为前缀,后面紧接是静态库名(自己创建),扩展名为.a
我在此使用myhello作为静态库名,所以静态库文件名为:libmyhello.a
ar -crv libmyhello.a hello.o
ls//同样使用ls命令查看是否生成静态库文件
可以看到有libmyhello.a 文件,说明操作成功
第四步:在程序中使用静态库:
如何使用静态库内部的函数:
只需要在使用到这些公用函数的源程序中包含这些公用函数的原型声明,然后再使用gcc命令生成目标文件指明静态库名,gcc便会从静态库中将公用函数链接到目标函数中
在这里我使用:
gcc main.c libmyhello.a -o hello
生成目标文件hello,然后运行:
说明我们的静态库中的公用函数已经连接到目标文件中了
第五步:由.o文件创建动态库文件:
动态库的命名规则和静态库类似,但其扩展名为**“.so”。这里我创建:”libmyhello.so"**动态库
gcc -shared -fPIC -o libmyhello.so hello.o//这里的-o不可少
ls
可以看到生成了**“libmyhello.so”**文件
第六步:在程序中使用动态库:
gcc -o hello main.c -L. -lmyhello
但是发现错误:
发现找不到动态库文件**”libmyhello.so"**,将文件复制到/usr/lib中,运行发现成功:
最后实验:当静态库和动态库同名时,gcc命令会使用哪个库文件呢?
发现:当静态库和动态库同名时,gcc命令优先使用动态库,默认去连/usr/lib目录中的动态库。
Linux下静态库.a与.so库文件的生成与使用:
第一步:创建一个作业目录以此保存本次的练习文件:
#mkdir test2
#cd test2
第二步:使用vim创建四个所需的文件:A1.c、A2.c、A.h、test.c
A1.c:
A2.c:
A.h:
test.c:
第三步:生成目标文件:A1.o、A2.o,并且生成静态库.a文件:
gcc -c A1.c A2.c
ar crv libafile.a A1.o A2.o
可以看到:
第四步:使用.a库文件,创建可执行文件test(若采用此种方法,需要保证生成的.a文件与.c文件存放在同一目录下)
共享库.so文件的生成与使用:
gcc -c -fpic A1.c A2.c //生成目标文件(此处生成.o文件必须添加”-fpic",否则在生成.so文件时会出错)
gcc -shared *.o -o libsofile.so //生成共享库.so文件
gcc -o test test.c libsofile.so //使用.so库文件,创建可执行程序
./test
发现错误:
运行ldd test,查看链接情况:
确实发现找不到对应的.so文件
这是由于Linux自身系统设定的相应的设置的原因,即其只在/lib and /user/lib 下搜索对应的.so 文件,故需将对应的so文件拷贝到对应路径:
也可以使用:“gcc -o test test.c -L. -l+**name,**来使用相应的库文件:
-L:表示在当前目录下,可自行定义路径path,即使用-Lpath即可。
-lname:name:即对应库文件的名字(除开lib)
2)在第一次作业的程序代码基础进行改编,除了x2x函数之外,再扩展写一个x2y函数(功能自定),main函数代码将调用x2x和x2y ;将这3个函数分别写成单独的3个 .c文件,并用gcc分别编译为3个.o 目标文件;将x2x、x2y目标文件用 ar工具生成1个 .a 静态库文件, 然后用 gcc将 main函数的目标文件与此静态库文件进行链接,生成最终的可执行程序,记录文件的大小。
第一步:创建四个子程序文件sub1.c、sub2.c、sub.h 、main.c
sub1.c:
sub2.c:
sub.h:
main.c:
第二步:生成目标.o文件并创建静态库:
gcc -c sub1.c sub2.c
ar crv libsub.a sub1.o sub2.o
gcc -o main main.c libsub.a
运行结果如下:
3)将x2x、x2y目标文件用 ar工具生成1个 .so 动态库文件, 然后用 gcc将 main函数的目标文件与此动态库文件进行链接,生成最终的可执行程序,记录文件的大小,并与之前做对比。
gcc -c -fpic sub1.c sub2.c
gcc -shared *.o -o libsub.so
gcc -o main main.c libsub.so
./test
通过ldd 生成可执行程序test 发现libsub.so找不到,于是将libsub.so 文件复制到/sur/lib目录下:
二. Gcc不是一个人在战斗。
说明gcc编译工具集中各软件的用途,了解EFF文件格式。学习任务如下:阅读、理解和学习材料“Linux GCC常用命令.pdf”和“GCC编译器背后的故事.pdf”,如实仿做一遍。
Linux GCC常用命令:
GCC(GNU C Compiler)是编译工具。本文所要介绍的将 C/C++语言编写的程序 转换成为处理器能够执行的二进制代码的过程即由编译器完成
一组二进制程序处理工具,包括:addr2line、ar、objcopy、objdump、as、ld、 ldd、readelf、 size 等。这 一组工具 是开发和 调试不可 缺少的工具 ,分别简介如下:
- addr2line:用 来将程序 地址转 换成其所 对应的程 序源文 件及所对 应的代 码 行,也可以得到所对应的函数。该工具将帮助调试器在调试的过程中定位对 应的源代码位置
- as:主要用于汇编
- ld:主要用于链接
- ar:主要用于创建静态库
- ldd:可以用于查看一个可执行程序依赖的共享库
- objcopy:将一种对象文件翻译成另一种格式,譬如将.bin 转换成.elf、或 者将.elf 转换成.bin 等
- objdump:主要的作用是反汇编。有关反汇编的详细介绍
- readelf:显示有关 ELF 文件的信息
- size:列出可执行文件每个部分的尺寸和总尺寸,代码段、数据段、总大小等
GCC编译器背后的故事:
第一步:准备工作
第二步:编译过程
-
预处理:
将所有的#define 删除,并且展开所有的宏定义,并且处理所有的条件预编 译指令,比如#if #ifdef #elif #else #endif 等
处理#include 预编译指令,将被包含的文件插入到该预编译指令的位置。
删除所有注释“//”和“/* */”
添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号。
保留所有的#pragma 编译器指令,后续编译过程需要使用它们。
使用gcc命令进行预处理操作:
gcc -E Hello.c -o Hello.i//-E使得GCC在进行预处理后即停止
-
编译:
使用gcc进行编译的命令如下:
gcc -S Hello.i -o Hello.s//-S使得GCC在执行完编译后停止,生成汇编程序
-
汇编:
使用GCC进行汇编的命令:
gcc -c Hello.s -o Hello.o//—C使得GCC在执行完汇编后停止,生成目标文件
或者直接调用as进行汇编:
as -c Hello.s -o Hello.o//使用Binutils中的as将Hello.s文件汇编成目标文件
注意:Hello.o目标文件为ELF格式的可重定向文件。
-
链接:
其中链接也分静态链接和动态链接:
静态链接是指在编译阶段直接把静态库加入到可执行文件中去,这样可执行 文件会比较大。链接器将函数的代码从其所在地(不同的目标文件或静态链 接库中)拷贝到最终的可执行程序中。为创建可执行文件,链接器必须要完 成的主要任务是:符号解析(把目标文件中符号的定义和引用联系起来)和 重定位(把符号定义和内存地址对应起来然后修改所有对符号的引用)。
动态链接则是指链接阶段仅仅只加入一些描述信息,而程序执行时再从系统 中把相应动态库加载到内存中去。
如果使用命令“gcc hello.c -o hello”则会使用动态库进行链接,生成的 ELF 可执行文件的大小(使用 Binutils 的 size 命令查看)和链接的动态库 (使用 Binutils 的ldd命令查看):
gcc Hello.c -o Hello size Hello//使用size查看大小 ldd Hello//可以看出该可执行文件链接很多其他的动态库,主要是Linux的glibe动态库
如 果 使 用 命 令 “ gcc -static hello.c -o hello”则 会 使 用 静 态 库 进 行 链 接 , 生成的 ELF 可执行文件的大小(使用 Binutils 的 size 命令查看)和链接的 动态库(使用 Binutils 的 ldd 命令查看)如下所示:
第三步:分析ELF文件
-
ELF文件的段:
一个典型的ELF文件包含下面几个段:
.text:已编译程序的指令代码段
.rodata:ro 代表 read only,即只读数据(譬如常数 const)
.data:已初始化的 C 程序全局变量和静态局部变量
.bss:未初始化的 C 程序全局变量和静态局部变量
.debug:调试符号表,调试器用此段的信息帮助调试
readelf -S Hello//可以查看其各个section的信息
-
反汇编ELF:
由于ELF文件无法被当做普通文件打开,如果希望直接查看一个ELF文件包含的指令和数据,需要使用反汇编的方法
objdump -D Hello gcc -o Hello -g Hello.c//要加上——g选项 objdump -S Hello
三. 编写一个C程序,重温全局常量、全局变量、局部变量、静态变量、堆、栈等概念
在Ubuntu(x86)系统和STM32(Keil)中分别进行编程、验证(STM32 通过串口printf 信息到上位机串口助手) 。
1)归纳出Ubuntu、stm32下的C程序中堆、栈、全局、局部等变量的分配地址,进行对比分析;
2)加深对ARM Cortex-M/stm32F10x的存储器地址映射的理解。下图是一个Cortex-M4的存储器地址映射示意图(与Cortex-M3/stm32F10x基本相同,只存在微小差异)
全局变量
在所有函数外部定义的变量称为全局变量(Global Variable),它的作用域默认是整个程序,也就是所有的源文件。
局部变量
定义在函数内部的变量称为局部变量(Local Variable),它的作用域仅限于函数内部, 离开该函数的内部就是无效的,再使用就会报错。、
全局变量 | 局部变量 | |
---|---|---|
定义位置 | 在方法外部,直接写在类中 | 在方法内部 |
作用范围 | 整个类中都可以使用 | 只能在方法中使用 |
默认值 | 如果没有赋值,则有默认值,规则同数组 | 没有默认值,要使用必须手动赋值 |
内存位置 | 位于堆内存 | 位于栈内存 |
静态变量
是在程序执行期间一直存在的变量。它们的生命周期与程序的整个运行时间相同,不受函数的调用和返回影响。静态变量在内存中的位置固定,多次调用函数时,静态变量的值会保持不变。
堆
是一块用于动态分配内存空间的区域。堆的内存空间由程序员手动分配和释放,用来存储一些动态的数据结构,如对象和数组。在堆上分配的内存由程序员手动释放,否则会造成内存泄漏。
栈
是一种用于存储函数调用和局部变量的内存区域。每当函数被调用时,栈会分配一块局部变量的内存空间,该空间会在函数执行完毕后自动释放。栈采用先进后出(LIFO)的数据结构,函数调用时,会一层层地将数据压入栈中,而返回时则按相反的顺序弹出。栈的大小通常是有限的,当栈溢出时,会导致程序异常终止。
对比点 | 堆 | 栈 |
---|---|---|
管理方式 | 堆的申请和释放工作由程序员控制 | 栈由操作系统自动分配释放 |
空间大小 | 大 | 小 |
生长方向 | 堆的生长方向向上,内存地址由低到高 | 栈的生长方向向下,内存地址由高到低 |
分配方式 | 堆都是动态分配的 | 栈的动态分配和堆是不同的,它的动态分配是由操作系统进行释放 |
分配效率 | 堆则是由C/C++提供的库函数或运算符来完成申请与管理,实现机制较为复杂 | 栈由操作系统自动分配,会在硬件层级对栈提供支持 |
存放内容 | 堆,一般情况堆顶使用一个字节的空间来存放堆的大小,而堆中具体存放内容是由程序员来填充的 | 栈存放的内容,函数返回地址、相关参数、局部变量和寄存器内容等 |
堆和栈相比,由于大量malloc()/free()或new/delete的使用,容易造成大量的内存碎片,并且可能引发用户态和核心态的切换,效率较低。栈相比于堆,在程序中应用较为广泛,最常见的是函数的调用过程由栈来实现,函数返回地址、EBP、实参和局部变量都采用栈的方式存放。虽然栈有众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,主要还是用堆。
Ubuntu环境下的变量分配:
#include <stdio.h>
#include <stdlib.h>
//定义全局变量
int init_global_a = 1;
int uninit_global_a;
static int inits_global_b = 2;
static int uninits_global_b;
void output(int a)
{
printf("hello");
printf("%d",a);
printf("\n");
}
int main( )
{
//定义局部变量
int a=2;//栈
static int inits_local_c=2, uninits_local_c;
int init_local_d = 1;//栈
output(a);
char *p;//栈
char str[10] = "yaoyao";//栈
//定义常量字符串
char *var1 = "1234567890";
char *var2 = "abcdefghij";
//动态分配——堆区
int *p1=malloc(4);
int *p2=malloc(4);
//释放
free(p1);
free(p2);
printf("栈区-变量地址\n");
printf(" a:%p\n", &a);
printf(" init_local_d:%p\n", &init_local_d);
printf(" p:%p\n", &p);
printf(" str:%p\n", str);
printf("\n堆区-动态申请地址\n");
printf(" %p\n", p1);
printf(" %p\n", p2);
printf("\n全局区-全局变量和静态变量\n");
printf("\n.bss段\n");
printf("全局外部无初值 uninit_global_a:%p\n", &uninit_global_a);
printf("静态外部无初值 uninits_global_b:%p\n", &uninits_global_b);
printf("静态内部无初值 uninits_local_c:%p\n", &uninits_local_c);
printf("\n.data段\n");
printf("全局外部有初值 init_global_a:%p\n", &init_global_a);
printf("静态外部有初值 inits_global_b:%p\n", &inits_global_b);
printf("静态内部有初值 inits_local_c:%p\n", &inits_local_c);
printf("\n文字常量区\n");
printf("文字常量地址 :%p\n",var1);
printf("文字常量地址 :%p\n",var2);
printf("\n代码区\n");
printf("程序区地址 :%p\n",&main);
printf("函数地址 :%p\n",&output);
return 0;
}
栈区的地址存储是向上增长,堆区的地址存储也是向上增长
STM32(Keil)环境下的变量分配:
#include "led.h"
#include "delay.h"
#include "key.h"
#include "sys.h"
#include "usart.h"
#include <stdio.h>
#include <stdlib.h>
//定义全局变量
int init_global_a = 1;
int uninit_global_a;
static int inits_global_b = 2;
static int uninits_global_b;
void output(int a)
{
printf("hello");
printf("%d",a);
printf("\n");
}
int main(void)
{
u16 t;
u16 len;
u16 times=0;
delay_init(); //延时函数初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
uart_init(115200); //串口初始化为115200
LED_Init(); //LED端口初始化
KEY_Init(); //初始化与按键连接的硬件接口
while(1)
{
//定义局部变量
int a=2;
static int inits_local_c=2, uninits_local_c;
int init_local_d = 1;
output(a);
char *p;
char str[10] = "yaoyao";
//定义常量字符串
char *var1 = "1234567890";
char *var2 = "abcdefghij";
//动态分配
int *p1=malloc(4);
int *p2=malloc(4);
//释放
free(p1);
free(p2);
printf("栈区-变量地址\n");
printf(" a:%p\n", &a);
printf(" init_local_d:%p\n", &init_local_d);
printf(" p:%p\n", &p);
printf(" str:%p\n", str);
printf("\n堆区-动态申请地址\n");
printf(" %p\n", p1);
printf(" %p\n", p2);
printf("\n全局区-全局变量和静态变量\n");
printf("\n.bss段\n");
printf("全局外部无初值 uninit_global_a:%p\n", &uninit_global_a);
printf("静态外部无初值 uninits_global_b:%p\n", &uninits_global_b);
printf("静态内部无初值 uninits_local_c:%p\n", &uninits_local_c);
printf("\n.data段\n");
printf("全局外部有初值 init_global_a:%p\n", &init_global_a);
printf("静态外部有初值 inits_global_b:%p\n", &inits_global_b);
printf("静态内部有初值 inits_local_c:%p\n", &inits_local_c);
printf("\n文字常量区\n");
printf("文字常量地址 :%p\n",var1);
printf("文字常量地址 :%p\n",var2);
printf("\n代码区\n");
printf("程序区地址 :%p\n",&main);
printf("函数地址 :%p\n",&output);
return 0;
}
}
在STM32下,栈区的地址存储是向下增长,堆区的地址存储却是向上增长。
inits_global_b);
printf(“静态内部无初值 uninits_local_c:%p\n”, &uninits_local_c);
printf(“\n.data段\n”);
printf(“全局外部有初值 init_global_a:%p\n”, &init_global_a);
printf(“静态外部有初值 inits_global_b:%p\n”, &inits_global_b);
printf(“静态内部有初值 inits_local_c:%p\n”, &inits_local_c);
printf(“\n文字常量区\n”);
printf(“文字常量地址 :%p\n”,var1);
printf(“文字常量地址 :%p\n”,var2);
printf(“\n代码区\n”);
printf(“程序区地址 :%p\n”,&main);
printf(“函数地址 :%p\n”,&output);
return 0;
}
}
**在STM32下,栈区的地址存储是向下增长,堆区的地址存储却是向上增长。**
##