二、Linux GCC编译、交叉编译与GDB调试
实验平台
l VMware workstation虚拟机内嵌口袋云系统,快速创建多种操作系统的虚拟机:(操作系统:Ubuntu18.04
CentOS,Windows10或Windows11)
l 实验目的
l 学习Linux系统下编写c语言文件、编译文件、
l 学习编写shell程序实现系统功能
实验准备
l VMware workstation虚拟机+Ubuntu18.04系统,安装gcc编译器,交叉编译器安装包。
实验案例:
1. PC平台的gcc编译过程
首先打开终端,输入gcc -v,显示当前系统的gcc版本:
然后创建一个c语言源文件hello.c:
# include <stdio.h>
int main()
{
printf("Hello Linux\n");
return 0;
}
执行gcc hello.c -o hello,然后直接./hello
2. GCC创建和使用静态链接库
1)首先进入第二个实验目录second,然后创建4个文件add.c, sub.c,div.c,test.h
/*****add.c*****/
#include "test.h"
int add(int a,int b)
{
return a + b;
}
/*****sub.c*****/
#include "test.h"
int sub(int a,int b)
{
return a - b;
}
/*****div.c*****/
#include "test.h"
int div(int a,int b)
{
return a / b;
}
/*****test.h*****/
#ifndef __TEST_H_
#define __TEST_H_
int add(int a,int b);
int sub(int a,int b);
int div(int a,int b);
#endif
2) 编译生成目标文件gcc -c *.c
*.c表示所有以.c结尾的文件,也即所有的源文件。执行完该命令,会发现 second 目录中多了三个目标文件,分别是 add.o、sub.o 和 div.o。
3)把所有目标文件打包成静态库文件:
ar rcs libtest.a *.o
*.o表示所有以.o结尾的文件,也即所有的目标文件。执行完该命令,发现 test 目录中多了一个静态库文件 libtest.a。
4)创建新的目录结构:在second目录下创建math目录,在math下创建include, lib, src三个目录,并将test.h移动到include下:
在src目录下创建一个新的测试文件main.c,在 main.c 中,可以像下面这样使用 libtest.a 中的函数::
/*****main.c*****/
#include <stdio.h>
#include "test.h" //必须引入头文件
int main(void)
{
int m, n;
printf("Input two numbers: ");
scanf("%d %d", &m, &n);
printf("%d+%d=%d\n", m, n, add(m, n));
printf("%d-%d=%d\n", m, n, sub(m, n));
printf("%d÷%d=%d\n", m, n, div(m, n));
return 0;
}
在编译 main.c 的时候,我们需要使用-I(大写的字母i)选项指明头文件的包含路径,使用-L选项指明静态库的包含路径,使用-l(小写字母L)选项指明静态库的名字。所以,main.c 的完整编译命令为:
gcc src/main.c -I include/ -L lib/ -l test -o math.out
注意,使用-l选项指明静态库的名字时,既不需要lib前缀,也不需要.a后缀,只能写 test,GCC 会自动加上前缀和后缀。
打开 math 目录,发现多了一个 math.out 可执行文件,使用./math.out命令就可以运行 math.out 进行数学计算。
3. GCC 生成和使用动态链接库
1)从文件中生成动态库
从单个源文件生成动态链接库:
$ gcc -fPIC -shared xxx.c -o libxxx.so
从目标文件生成动态链接库:
$ gcc -fPIC -c xxx.c -o xxx.o
$ gcc -shared xxx.c -o libxxx.so
从多个文件生成动态库:
gcc -fPIC -shared xxx1.c xxx2.c xxx3.c -o libxxx.so
或:gcc -fPIC -shared xxx1.o xxx2.o xxx3.o -o libxxx.so
2)接上面第2个实例,首先已经编译生成的目标文件不能再用,因为不是按照动态链接方式生成的,如果需要就还需要再次重新编译目标文件:
gcc -fPIC -c add.c -o add.o
3)在此我们直接用源文件来重新编译直接生成动态链接库文件:
gcc -fPIC -shared add.c sub.c div.c -o libtest.so
4)使用动态链接库:
重新对测试程序main.c编译,这次采用动态链接库:
gcc src/main.c lib/libtest.so -o math2
生成后执行math2:
4.安装交叉编译环境
1)修改系统设置
设置->Software&Updates
点击Download from的先择框,选择other
选择China中的任意一个源,可选择阿里云。输入用户密码之后,点击close,再选择Reload,等待下载完成后即可。
- 安装更新sudo apt-get update
- 安装依赖包
sudo apt-get install git gnupg flex bison gperf build-essential zip curl libc6-dev x11proto-core-dev u-boot-tools libx11-dev:i386 libreadline6-dev:i386 libgl1-mesa-glx:i386 libgl1-mesa-dev g++-multilib mingw32 tofrodos libncurses5-dev python-markdown libxml2-utils xsltproc zlib1g-dev:i386
若因电脑配置问题出现mingw32安装错误,可参考:
4)复制交叉编译器并解压
把交叉编译器压缩包arm-2009q3.tar.gz拷贝至 Ubuntu 主机工作目录下,执行下面的命令安装到/usr/local/arm 目录下。
# sudo mkdir -p /usr/local/arm
# sudo tar -zxvf arm-2009q3.tar.gz -C /usr/local/arm
查看编译器安装情况表明已经正常解压完成:
配置环境变量
#sudo gedit /etc/bash.bashrc
在最后加上
export PATH=$PATH:/usr/local/arm/arm-2009q3/bin
export PATH
启动环境变量设置:
#source /root/.bashrc
如没有权限,则可换用
sudo –s
source /etc/bash.bashrc
检查是否设置成功:
#echo $PATH
#arm-none-linux-gcc –v
如果打印显示arm编译器版本信息,表示安装成功。
5)应用并检验交叉编译器
arm-none-linux-gnueabi-gcc -c add.c -o addarm
然后查看新生成的addarm文件属性:file addarm,可见其为ARM格式,在PC上直接执行会报错:bash: ./addarm: cannot execute binary file: Exec format error
思考与练习题
1.
2.
三、Linux maikefile脚本编写
实验平台
l VMware workstation虚拟机内嵌口袋云系统,快速创建多种操作系统的虚拟机:(操作系统:Ubuntu18.04
CentOS,Windows10或Windows11)
l实验目的
l 学习Linux系统下编写makefile文件
l 学习企业级项目中工程编译管理
实验准备
l VMware workstation虚拟机+Ubuntu18.04系统,安装gcc编译器。
实验案例:
- 按照makefile显式规则编写
对上一章中math目录下已经建立好目录结构的多个文件编写显式规则的makefile,首先将second目录下的所有文件复制到third目录下,然后在math目录下编写:
math3: src/main.o src/add.o src/sub.o src/div.o
gcc src/main.o src/add.o src/sub.o src/div.o -o math3
main.o:src/main.c include/test.h
gcc -c src/main.c -o src/main.o
add.o:add.c
gcc -c src/add.c -o src/add.o
sub.o:sub.c
gcc -c src/sub.c -o src/sub.o
div.o:div.c
gcc -c src/div.c -o src/div.o
clean:
rm src/*.o math3
保存后执行:
- 采用隐含规则和自动化变量编写企业级项目管理makefile文件
1)首先复制math目录,并重命名为math4,在其中创建一个目录output用于保存生成文件,创建一个makefile文件,输入以下内容:
VERSION = 1.0.0 #程序版本号
SOURCE = $(wildcard ./src/*.c) #获取所有的.c文件
OBJ = $(patsubst %.c, %.o, $(SOURCE)) #将.c文件转为.o文件
INCLUDES = -I./include #头文件路径
LIBS = -libtest.so #库文件名字
LIB_PATH = -L./lib #库文件地址
DEBUG = -D_MACRO #宏定义
CFLAGS = -Wall -c #编译标志位
TARGET = math4
CC = gcc
$(TARGET): $(OBJ)
#mkdir -p output/ #创建一个目录,用于存放已编译的目标
$(CC) $(OBJ) -o output/$(TARGET)
%.o: %.c
$(CC) $(INCLUDES) $(DEBUG) $(CFLAGS) $< -o $@
.PHONY: clean
clean:
rm -rf $(OBJ) output/
2)说明:
版本:软件开发过程中,会产生多个版本程序,通常会在程序末尾加上版本号后缀。
VERSION = 1.0.0 #定义
$(CC) $(OBJ) $(LIB_PATH) $(LIBS) -o output/$(TARGET).$(VERSION) #使用
头文件:由于.c文件与.h文件分开在不同目录下,所以应指定头文件路径。INCLUDES = -I ./include
宏定义:在代码调试的过程中,我们通常会加个宏定义来控制此段代码是否被编译,比如:#ifdef _MACRO
printf("macro test\n");
#endif
具体的宏我们可不定义在代码里,可在Makefile里指定,比如:
DEBUG = -D_MACRO #定义
$(CC) $(INCLUDES) $(DEBUG) $(CFLAGS) $< -o $@ #使用
编译选项:当编译选项较多时,我们通常会把它单独拿出来,比如:
CFLAGS = -Wall -c #定义
$(CC) $(INCLUDES) $(DEBUG) $(CFLAGS) $< -o $@ #使用
库:代码里如果要使用到库,我们可以将库名字和路径分别拿出来,比如:
LIBS = -libtest #库文件名字
LIB_PATH = -L./lib #库文件地址
$(CC) $(OBJ) $(LIB_PATH) $(LIBS) -o output/$(TARGET).$(VERSION) #使用
output目录:如果不想把生成的程序与源文件混在一起,可将生成的程序单独放在一个output目录
思考与练习题
1.
2.
四、Linux字符设备驱动程序开发
实验平台
l VMware workstation虚拟机内嵌口袋云系统,快速创建多种操作系统的虚拟机:(操作系统:Ubuntu18.04
CentOS,Windows10或Windows11)
l 实验目的
l 学习Linux系统下编写标准设备驱动程序
l 学习Linux系统下编写设备树类型驱动程序
实验准备
l VMware workstation虚拟机+Ubuntu18.04系统,安装gcc编译器和交叉编译器,连接开发板。
实验案例:
- 编写Linux 2.6内核版本以下标准设备驱动程序
1)gedit hello.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#define HELLO_MAJOR 231
#define DEVICE_NAME "HelloModule"
static int hello_open(struct inode *inode, struct file *file){
printk(KERN_EMERG "hello open.\n");
return 0;
}
static ssize_t hello_write(struct file *file, const char __user * buf, size_t count, loff_t *ppos){
printk(KERN_EMERG "hello write.\n");
return 0;
}
static struct file_operations hello_flops = {
.owner = THIS_MODULE,
.open = hello_open,
.write = hello_write,
};
static int __init hello_init(void){
int ret;
ret = register_chrdev(HELLO_MAJOR,DEVICE_NAME, &hello_flops);
if (ret < 0) {
printk(KERN_EMERG DEVICE_NAME " can't register major number.\n");
return ret;
}
printk(KERN_EMERG DEVICE_NAME " initialized.\n");
return 0;
}
static void __exit hello_exit(void){
unregister_chrdev(HELLO_MAJOR, DEVICE_NAME);
printk(KERN_EMERG DEVICE_NAME " removed.\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
驱动文件主要包括函数hello_open、hello_write、hello_init、hello_exit,测试案例中并没有赋予驱动模块具有实际意义的功能,只是通过打印日志的方式告知控制台一些调试信息,这样我们就可以把握驱动程序的执行过程。
在使用printk打印的时候,在参数中加上了“KERN_EMERG”可以确保待打印信息输出到控制台上。由于printk打印分8个等级,等级高的被打印到控制台上,而等级低的却输出到日志文件中。
2) 编写驱动所需的Makefile(注意M要大写,不要小写)
ifneq ($(KERNELRELEASE),)
MODULE_NAME = hellomodule
$(MODULE_NAME)-objs := hello.o
obj-m := $(MODULE_NAME).o
else
KERNEL_DIR = /lib/modules/`uname -r`/build
MODULEDIR := $(shell pwd)
.PHONY: modules
default: modules
modules:
make -C $(KERNEL_DIR) M=$(MODULEDIR) modules
clean distclean:
rm -f *.o *.mod.c .*.*.cmd *.ko
rm -rf .tmp_versions
endif
说明:
uname -r用于输出当前系统内核版本
3)编写测试文件应用代码hellotest.c
#include <fcntl.h>
#include <stdio.h>
int main(void)
{
int fd;
int val = 1;
fd = open("/dev/hellodev", O_RDWR);
if(fd < 0){
printf("can't open!\n");
}
write(fd, &val, 4);
return 0;
}
首先打开设备文件,然后向设备中写入数据。如此,则会调用驱动中对应的xxx_open和xxx_write函数,通过驱动程序的打印信息可以判断是否真的如愿执行了对应的函数。
- 编译和测试
测试的方法整体来说就是,编译驱动和上层测试应用;加载驱动,通过上层应用调用驱动;最后,卸载驱动。
4.1)编译驱动
#make
make命令,直接调用Makefile编译hello.c,最后会生成“hellomodule.ko”。
如果出现错误:arch/x86/Makefile:168: CONFIG_X86_X32 enabled but no binutils support,
则gedit ~/.bashrc把交叉编译环境暂时注释掉,然后关闭终端再重新打开即可使用PC上自带的原生gcc编译器。
4.2)编译上层应用
#gcc hellotest.c -o hellotest
通过这条命令,就能编译出一个上层应用hellotest。
4.3)加载驱动
#insmod hellomodule.ko
insmod加载驱动的时候,会调用函数hello_init(),可在终端上打印出调试信息。
如果看不到调试信息,有两种可能性:
- 一般这种情况出现在发行版Linux(如Ubuntu)驱动开发中,出现这个问题的原因是:printk默认的输出设备是/dev/console,而这个设备只能在内核中访问。我们用户使用的控制台,也叫终端,对应到的是/dev/tty。可以通过tty命令来查看当前使用的是哪个终端设备:echo "hello"> /dev/tty来查看这个终端设备的打印作用。
由于发行版Linux操作系统基本都是使用grub来进行磁盘引导,而默认的grub中并没有指定console的映射终端,这就导致很多人在网络上面搜索到的B)那样的解决方法,通过修改printk控制台输出信息级别没有任何效果。
此时可在终端中输入dmesg命令打开日志查看内核打印信息。
B)则是因为内核打印是需要按照级别设置的。
使用printk时,Linux内核根据日志级别,可能把消息打印到当前控制台上,这个控制台是一个字符设备。这些消息从终端输出的前提是日志输出级别小于console_loglevel(越小级别越高)。日志级别有八个:0-7,
通过读写/proc/sys/kernel/printk文件可以读取、修改控制台的日志级别。查看这个文件:
cat /proc/sys/kernel/printk
上面显示的数字:6、7、1、6分别对应控制台日志级别、默认的消息日志级别、最低的控制台日志级别和默认的控制台日志级别。可以使用下面命令设置当前日志级别:
echo 8 > /proc/sys/kernel/printk
这样所有级别小于8的都可以打印在控制台终端上了
Insmod加载后在"/proc/devices"中可以看到已经加载的模块。
4.4)创建节点
虽然已经加载了驱动hellomodule.ko,而且在/proc/devices文件中也看到了已经加载的模块HelloModule,但是这个模块仍然不能被使用,因为在设备目录/dev目录下还没有它对应的设备文件。所以,需要创建一个设备节点。
#mknod /dev/hellodev c 231 0
在/proc/devices中看到HelloModule模块的主设备号为231,创建节点的时候就是将设备文件/dev/hellodev与主设备号建立连接。这样在应用程序操作文件/dev/hellodev的时候,就会定位到模块HelloModule。
/proc/devices 与 /dev的区别
- /proc/devices中的设备是驱动程序生成的,它可产生一个major供mknod作为参数。这个文件中的内容显示的是当前挂载在系统的模块。当加载驱动HelloModule的时候,并没有生成一个对应的设备文件来对这个设备进行抽象封装,以供上层应用访问。
- /dev下的设备是通过mknod加上去的,用户通过此设备名来访问驱动。我以为可以将/dev下的文件看做是硬件模块的一个抽象封装,Linux下所有的设备都以文件的形式进行封装。
4.5)上层应用调用驱动
#./hellotest
hellotest应用程序先打开文件“/dev/hellodev”,然后向此文件中写入一个变量val。期间会调用底层驱动中的hello_open和hello_write函数,hellotest的运行结果同样可通过命令dmesg查看,如下所示,看到打印hello_open和hello_write表示运行成功。
4.6) 卸载驱动
#rmmod hellomodule
insmod卸载驱动的时候,会调用函数hello_exit(),再次运行dmesg可以看到退出时的打印调试信息。
总结:
一个模块的操作流程:
(1)通过insmod命令注册module
(2)通过mknod命令在/dev目录下建立一个设备文件"xxx",并通过主设备号与module建立连接
(3)应用程序层通过设备文件/dev/xxx对底层module进行操作
注:上述所有驱动程序编译时调用的是x86的gcc编译器,也可以换成交叉编译器编译,并在编译成功后可下载到开发板上运行。
- 编写Linux 2.6内核版本以上设备树驱动程序
从 Linux 2.6 起引入了一套新的驱动管理和注册机制: Platform_device和 Platform_driver。从 Linux 2.6 起引入了一套新的驱动管理和注册机制:Platform_device 和 Platform_driver。Linux中大部分的设备驱动,都可以使用这套机制, 设备用Platform_device 表示,驱动用 Platform_driver进行注册。
platform 是一个虚拟的地址总线,相比 PCI、USB,它主要用于描述 SOC 上的片上资源。platform 所描述的资源有一个共同点,在 CPU 的总线上直接取址。Linux platform device driver 机制和传统的 device driver 机制(通过 device_register driver_register 函数进行注册)相比,一个十分明显的优势在于 platform 机制将设备本 身的资源注册进内核,由内核统一管理,在驱动程序中使用这些资源时通过 platform device 提供的 标准接口进行申请并使用。这样提高了驱动和资源管理的独立性,并且拥有较好的可移植性和安 全性(这些标准接口是安全的)。
Platform 机制的本身使用并不复杂,由两部分组成:platform_device 和 platfrom_driver。通过platform 机制开发底层驱动的大致流程为:
定义 platform_deviece -->注册 platform_device -->定义 platform_driver --> 注册
platform_driver。
其中设备树选项默认没有开启:
思考与练习题:
- 下
- 下