在Linux驱动程序选项上,或者我如何度过周末

“我们懒惰又好奇”




这次,发帖的原因是在一本专门针对Linux OS的好杂志(以下简称L)上的一篇文章,其中吸引的“专家”称赞了将LCD连接到Raspbery板的驱动程序。 由于这些事情(连接,而不是操作系统)属于我的专业兴趣范围,因此我仔细地阅读了这篇文章,然后找到了“驱动程序”的实际文本,并为获得IT称赞感到有些惊讶。 好吧,总的来说,专家级别可以确定,仅仅是因为他固执地称该程序为驱动程序,尽管事实并非如此。 似乎和他的无花果一样,您永远都不知道有人为自己写什么,而是将其发布到公共领域-“我不知道这是可能的。”

I2C总线上的设备地址直接在程序文本中设置,并且需要更改才能重新编译(这不是整个内核,这很好),这一事实尤其令人高兴。 顺便说一句,我注意到在致力于L的论坛上,关于软件问题的最流行答案是“重建最新的内核版本”。 这种方法对我来说似乎有些奇怪,可能是我不了解。 但是,尽管如此,还是出现了一个问题,即如何在A中真正实现驱动程序的参数化(内部而不是外部-一切都很简单明了),这正是本文的答案。

不是我一直在为L驱动程序编写驱动程序,而是整个过程我很熟悉,并且google证实了模糊的回忆,创建模块源代码时应该使用一组宏,以便能够将操作参数传递给它,例如,将设备地址传递给去公交车。 但是,该过程本身的机制并未在任何地方进行描述。 我在许多链接中看到了相同的文本(顺便说一句,一个有趣的问题-为什么这样做,也就是说,将别人的文本片段放在我的资源上-我真的不理解此操作的含义),它描述了上面的宏。 我没有提到执行该操作的机制,对于另一个众所周知的操作系统(Windows),我不得不陈述一个事实,并仅限于此,但是A的优点之一是源文本的可用性以及能够找到有关其内部结构的任何问题的答案的能力,我们将做什么。 我立即注意到,我将尽量不要重复从其他来源获得的信息,并且我仅将自己限于理解文本所必需的信息。

但是,在您查看源代码之前,我们会先想一想,但是如果我们要完成类似的任务,我们会怎么做(突然,在这篇文章之后,他们将邀请我加入L矿工,并且您不会拒绝)。 因此,有可能创建一个模块-某些经过特殊设计的程序单元,可以使用某些系统实用程序(insmode-以下简称I)将其加载到内存中执行,同时将字符串作为启动参数传递。 此行可以包含严格定义的词法单元,在创建模块的源文本时会指定其格式说明,并且这些单元包含的信息使您可以更改此模块的内部变量的值。

让我们更仔细地考虑描述上述词汇单元的方式,我们需要它来考虑各种解决方案。 通过调用宏来确定分析单位,该宏将获得必要的信息-设置过程中必须修改的变量名称,其外部名称(通常与上一个变量相同),有限集中变量的类型以及以rw-rw-rw样式访问变量的权限。 另外,可以指定描述变量的(可选)文本字符串。 显然,此信息是必要和充分的(结合语法单元的设计规则-分隔符和标记)来构建以文本字符串形式指定的参数列表的解析器,但为过程参与者之间的功能分配实现留出了空间。

要配置模块,我们需要:

  1. 表格(嗯,这是在编译阶段,您可以根据自己的喜好来做,尽管这样做仍然很有趣),然后存储上述设置的表格,
  2. 根据此表解析输入参数,并
  3. 根据语法单元的分析结果,对某些内存区域进行更改。

我们将以“如果我是导演”的方式思考一下,并提出可能的实现方式。 我们如何实现系统实用程序和模块的相似行为-我们将以越来越复杂的方式开始对选项的分析。

第一个解决方案是And实用程序几乎什么也不做,只是调用它所指示的模块,然后以命令行样式将其余参数传递给它,而该模块已经根据它们中可用的信息进行了解析,并进行了必要的修改。 该解决方案简单,易于理解且相当可行,但必须考虑以下情况:绝不应该将参数的分析留给模块作者的意愿,因为这将为他提供不可接受的空间,并且毕竟,两个程序员将​​始终编写三个解析器选项。 因此,我们去见了他,允许他使用一个不确定类型的参数,该参数以文本字符串作为值。

因此,某个标准的解析器应该自动包含在模块的文本中,这在宏替换级别很容易实现。

该解决方案有两个缺点:

  1. 尚不清楚我们为什么需要它。而且,您可以立即从命令行使用参数调用该模块,
  2. 模块代码(初始化部分)必须包含所有三部分必要的信息,并且仅当模块启动且以后不再使用时,此信息才是必需的,并且始终占用空间。 立即保留该信息一定要占用文件中空间的保留,但是如果仔细地做所有事情,则在加载模块时它可能不会进入内存。 为了做到这一点,我们回想起_init和_initdata指令(顺便说一下,但是它们如何工作,我们必须弄清楚-这是下一篇文章的主题-您期待它吗?)。 但是在后一种情况下,文件中信息的第2部分和第3部分显然是多余的,因为相同的代码将出现在许多模块中,这是恶意违反DRY原理的。

由于存在明显的缺点,因此极不可能实施此选项。 此外,还不清楚为什么要在宏中设置有关参数类型的信息,因为模块本身非常了解其修改内容(尽管分析器在检查参数时可能需要修改)。 做出此决定的可能性的总体评估为2-3%。

关于指出的缺陷编号2的必要离题-那时我是一名专家,当时256 KB的RAM足以组织4个工作站,56 KB的具有双任务OS,而单任务的OS开始以16 KB工作。 好吧,650 kb足够用于任何程序,通常来自非科学小说领域。 因此,我习惯于认为RAM是一种稀缺资源,除非绝对必要(通常​​是性能要求),否则我非常反对RAM的浪费使用,在这种情况下,我不会观察到这种情况。 由于大多数读者的现实情况各不相同,因此您可能会对这种选择的偏好有自己的评估。

第二种解决方案-解析器本身传输到AND,AND将提取的数据传输到模块(其初始化部分)-参数号和值。 然后,我们保留了参数的一致性,并减少了对模块尺寸的要求。 问题仍然是,如何提供可能的参数的AND列表,但这由宏通过创建模块的预定结构以及块在某个特定位置(文件或内存)的位置来提供。 该解决方案比以前的解决方案更好,但是仍然保留了模块中多余的内存。 总的来说,我喜欢这种解决方案,因为我的解析器(比所有其他程序员差,我有自己的解析器,虽然没有缺陷,但绝对不会致命)根据此方案工作,将识别出的规则和值的数量返回给主程序参数。 但是,实施此特定选项的可能性不是很高-5%。

第二个解决方案的子选项是将提取的参数不传递到模块的开始部分,而是直接传递到其已加载的工作部分,例如通过ioctl-内存需求是相同的。 我们有一个独特的机会可以“即时”更改参数,而其他版本则没有实现。 尚不清楚我们为什么需要此功能,但看起来很漂亮。 缺点是:1)您需要为可能未使用的请求提前保留功能区的一部分,以及2)修改器代码必须始终存在于内存中。 估计实施可能性-百分比5。

第三种解决方案是将传递给,并且还要修改参数。 然后,在加载模块And的二进制代码的过程中,它可以修改中间存储器中的数据并将具有更改后的参数的驱动程序代码加载到永久部署的位置,或者直接在二进制文件已加载到的存储区域中进行这些修改,并且文件中存在的参数表位于内存中既可以加载也可以不占用它(请记住指令)。 该决定是负责任的,与前一个决定一样,它将要求模块和AND之间存在预定义的通信区域以存储参数的描述,但是它进一步减少了对模块中过多内存的需求。 马上,我们注意到这种解决方案的主要缺点-无法控制参数值及其一致性,但是没有什么可做的。 这是很正常的解决方案,很可能是75%。

第三种解决方案的一种变体-有关参数的信息不存储在模块本身中,而是存储在某些辅助文件中,因此模块中根本没有多余的内存。 原则上,当模块包含配置部分时,可以在先前版本中进行相同的操作,该配置部分在引导过程中使用AND,但未加载到包含模块实际可执行部分的RAM中。 与以前的版本相比,添加了一个额外的文件,并且不清楚我们要付多少钱,但是也许他们在发明之前就做了初始化指令-5%。

剩下的7%将留给我无法提出的其他选择。 好吧,既然我们的幻想已经耗尽(肯定的,如果有更多想法,请在评论中提问),我们将开始研究L的来源。

首先,我注意到,很显然,将源文本分发到文件中的技术与操作系统(16 kb大小)一起丢失了,因为目录结构,它们的名称和文件名与内容几乎没有关联。 考虑到嵌入的内含物的存在,在编辑的帮助下对下载源进行的经典研究变成了一个奇怪的任务,将毫无用处。 幸运的是,在线上有一个迷人的实用工具Elixir,它使您可以进行上下文搜索,并且在此过程变得更加有趣和富有成果。 我在elixir.bootlin.com网站上进行了进一步的研究。 是的,与kernel.org不同,该站点不是内核奶酪的官方集合,但我们希望它们的源代码相同。

首先,让我们看一下用于确定参数的宏-首先,我们知道它的名称,其次,它应该更容易(是的,现在)。 它位于moduleparam.h文件中-相当合理,但是鉴于我们稍后将看到的内容,这真是令人惊喜。 巨集

{0}module_param(name,type,perm) 

是一个包装

  {0a}module_param_named(n,n,t,p) 

-最常见的情况是语法糖。 同时,由于某种原因,在包装文本之前的注释中给出了其中一个参数的允许值的枚举,即变量的类型,而不是第二个宏,它确实起到了作用并且可以直接使用。

宏{0a}包含对三个宏的调用

 {1}param_check_##t(n,&v) 

(所有有效类型都有一组宏)

 {2}module_param_cb(n,&op##t,&v,p) 



 {3}__MODULE_PARM_TYPE(n,t) 

(请注意名称,但要注意其魅力),并​​且第一个名称在其他地方使用,也就是说,A的创建者也大胆地忽略了Occam和KISS原则的建议-显然,这是对未来的某种基础。 当然,这些只是宏,但是它们不花任何钱,但是仍然.....

顾名思义,三个宏{1}中的第一个检查参数类型的对应关系并包装

 __param_check(n,p,t) 

请注意,在包装的第一阶段,宏抽象的级别降低了,而在第二阶段,宏抽象的级别可能以不同的方式增加了,在我看来,这可能更简单,更合乎逻辑,尤其是考虑到在其他任何地方都没有使用平均宏。 好的,让我们提出另一种方法来检查储钱罐中的宏参数并继续前进。

但是接下来的两个宏实际上会生成参数表的元素。 您为什么不问两个,而不是一个,我很久以来一直不了解L.的创建者的逻辑,很可能是基于这两个宏的样式差异,从名称开始,后来添加了第二个宏以扩展功能并修改现有结构这是不可能的,因为起初他们很遗憾分配一个位置来指示选项参数。 与往常一样,宏{2}掩盖了我们的宏

 {2a}_module_param_call(MODULE_PARAM_PREFIX,n,ops,arg,p,-1,0) 

(有趣的是,除了8250_core.c之外,没有使用其他相同的参数直接调用此宏),但是后者已经生成了源代码。

简短说明-在搜索过程中,我们确保文本导航正常运行,但有两种不愉快的情况:按名称片段进行的搜索不起作用(虽然找到了check_param_byte,但未找到check_param_);而搜索仅对对象声明起作用(未找到变量,然后由ctrF在此文件中找到,但未检测到按源的内置搜索)。 不太令人鼓舞,因为我们可能需要在当前文件之外搜索对象,但是“最后,我们没有其他对象”。

由于存在以下两行,因此在已编译模块的文本中执行{1}

 module_param_named(name, c, byte, 0x444); module_param_named(name1, i, int, 0x444); 

出现以下类型的片段

 static const char __param_str_name[] = "MODULE" "." "name"; static struct kernel_param const __param_name \ __attribute__((__used__)) \ __attribute__ ((unused,__section__ ("__param"),aligned(sizeof(void *)))) \ = { __param_str_name, ((struct module *)0), &param_ops_byte, (0x444), -1, 0, { &c } }; static const char __UNIQUE_ID_nametype72[] \ __attribute__((__used__)) __attribute__((section(".modinfo"), unused, aligned(1))) \ = "parmtype" "=" "name" ":" "byte"; static const char __param_str_name1[] = "MODULE" "." "name1"; static struct kernel_param const __param_name1 \ __attribute__((__used__)) \ __attribute__ ((unused,__section__ ("__param"),aligned(sizeof(void *)))) \ = { __param_str_name1, ((struct module *)0), &param_ops_int, (0x444), -1, 0, { &i } }; static const char __UNIQUE_ID_name1type73[] __attribute__((__used__)) \ __attribute__((section(".modinfo"), unused, aligned(1))) \ = "parmtype" "=" "name1" ":" "int"; 

(实际上,这里生成了单行文件,为了便于查看,我将它们分成几行),我们可以立即说没有暗示包含解析器程序部分或用于在源文本中为参数分配值的模块的提示,因此选项1和2可以是被认为不在进一步考虑之列。 链接器的特殊属性的存在暗示了位于某个预定位置的通信区域的存在,通过该通信区域可以传输参数的描述。 同时,我们困惑地注意到,根本没有以解析器模块可以使用的文本形式对生成的可能参数块进行任何描述。 显然,编写良好的代码是自记录的,但程度不至于不会再增加选项1或2的可能性,而解析器是由模块开发人员编写的。

在最后生成的行中,同时使用__used__和未使用的属性的组合看起来很有趣,尤其是当您查看宏代码的下一个片段时

 #if GCC_VERSION < 30300 # define __used __attribute__((__unused__)) #else # define __used __attribute__((__used__)) #endif 

A的开发人员抽烟是什么样的敏捷性,他们的想法的痛苦曲折方式体现在代码中。 我知道您可以使用两种形式的属性编写,但是为什么要在同一行上执行-我不明白。

可以注意到结果代码的另一个有趣特征-重复有关变量名称及其类型的信息。 目前尚不清楚为什么要这样做,但事实本身并不令人怀疑。 当然,此信息是连贯的,因为它是在自动模式下构建的,并且当源文本发生更改时此连贯性将被保留(这是很好的),但是它是重复的(这是不好的),也许以后我们会理解这种解决方案的必要性。 同样,使用源代码的行号来形成唯一名称的需求仍然不清楚,因为第一条生成的行确实没有它。

另一个说明-确切地弄清参数的定义不是一件容易的事,但是由于有了MinGW,它仍然可以完成。 在引擎盖下有参数的字符串化和双重粘合,唯一名称的形成以及使用宏的其他棘手技巧,但我仅介绍结果。 总结中间结果,我可以说学习宏A不是我想以谋生为目的,它只能作为一种娱乐,但我们仍在继续。

在理解任务时,我们不会进一步提高对宏的理解,因此我们转向And实用工具的源代码,并尝试了解它的作用。

首先,我们惊奇地发现所需的奶酪没有包含在内核资源中。 是的,我准备同意I是一个实用程序,并且通过加载模块的入口点与内核进行交互,但是有关L驱动程序的任何书籍都告诉我们有关该实用程序的信息,因此缺少靠近内核源代码的“正式”版本的源代码会导致误会我 好吧,好吧,Google并没有让我们失望,我们都对奶酪一无所知。

第二个令人惊奇的事情是,该实用程序是由一个包构成的,该包的名称与名称没有任何关系,并且有多个这样的包,并且每个包在不同的地方以自己的方式命名-至少可以这样说。 如果您安装了L,则使用命令-您可以找到从哪个软件包中组装And实用程序,然后进行搜索,但是如果我们进行理论研究(出于某些原因,我个人不将L保留在家用计算机上,其中一些原因我说出了我的帖子(例如理论上的拳击手),然后这种方法对我们不可用,剩下的只是在Internet上进行搜索,所幸,它给出了结果。

好吧,第三点令人惊奇的是,实用程序名称本身不会出现在源代码中的任何位置,不会在文件名中使用,而只能在make文件中找到,我知道在C语言中我们必须将主函数命名为main,而这没有讨论(个人而言,我不在我很高兴,因为Pascal被宠坏了,但是他们在设计语言时没有提出我的意见),但是至少可以在注释中写出该实用程序的外部名称。 必要的注意-C语言中的很多事情都是按照“我们习惯”的原则完成的,有时可能很难使事情变得不同,甚至是不可能的,但是现在您可以做些什么,拖着没有把手的手提箱。

我们找到了两个包含源文本的软件包,并且还在github上找到了奶酪,我们发现它们是相同的,并确信这是实用程序源代码的外观。 接下来,我们仅研究git上的文件,特别是由于这里仅将其称为insmod.c,因此我们发现And首先,它将参数列表转换为一个长的以null终止的字符串,其中各个元素用空格分隔。 接下来,他调用了两个函数,第一个函数名为grub_file,显然会打开二进制文件,而第二个函数名为init_module,并使用带有二进制文件和参数字符串的指向打开文件的指针,称为load_module,这表明了此函数作为加载的目的。修改参数。

我们转到第二个函数的文本,它位于文件中...这是一个令人沮丧的东西-不在Geet上研究的存储库的任何文件中(嗯,这只是逻辑上的,这是内核的一部分,并且它不在这里),不是。 Google再次着急提供帮助,并将我们带回到Elixir和module.c文件下的内核奶酪。 应该指出的是,令人惊讶的是,包含用于处理模块的功能的文件的名称看起来合乎逻辑,我什至不知道该如何解释,这很可能是偶然发生的。

现在我们已经很清楚缺少文本了。在内核旁边-它实际上什么也没做,它只将参数从一种形式转移到另一种形式,然后将控制权转移到内核本身,因此,不值得躺在附近。 从这一刻开始,很明显没有关于参数结构的明确外部信息,因为内核通过其自己的宏跳过了它们,并且完全了解它们的所有信息,而其余的则不需要了解任何内部结构(鉴于源可供查看,一些评论不会受到损害,但是原则上,即使没有这些评论,它也确实越来越清晰),但到目前为止,它几乎从未对执行机制本身的实现有所了解。

注意-关于将控制权转移到内核方面,我有些激动,因为现在我们可以肯定地知道在内核源代码中使用该函数的情况,无论二进制部分是否将链接到模块,或者它是否位于内核映像本身中,这一点仍然未知。 通过SYSCALL_DEFINE3以特殊的方式构造该函数的入口点的事实间接证明了第二种选择,但是我早就明白,我关于逻辑和不合逻辑,可接受和不可接受以及关于允许和不可接受的观点非常重要。与L.开发人员的偏离

注意-内置搜索花园中还有一个小卵石-当搜索该宏的定义时,我看到很多地方都可以将其用作函数,其中,作为宏的定义非常隐蔽。

例如,我不明白为什么需要外部实用程序将操作系统的标准格式(agrc,argv)中的参数转换为以空格为分隔符的空终止字符串的形式,系统模块会进一步处理该方法-这种方法在某种程度上优于我的方法认知能力。 特别是考虑到用户输入的参数字符串为以零结尾的字符串形式,并以空格作为分隔符,内核中的实用程序将其转换为形式(argc,argv)。 很让人想起以前的笑话:“我们从炉子中取出水壶,倒出水,然后得到一个已知解决方案的问题。” 而且由于我一直坚持“不让您的对话者比您自己愚蠢,直到他证明相反的观点”这一原则。 而且即使在那之后,您也可能会误会,“对于A的开发人员而言,第一个词绝对是正确的,这意味着我误解了一些东西,但我不习惯。 如果有人可以对所说明的双重转换事实提供合理的解释,请在评论中提出。 但是我们继续调查。

选项1和2的实现前景变得“非常微弱”(最近一篇有关开发国内高速ADC前景的文章中的一句话),因为使用内核函数将模块加载到内存中,然后将控制权传递给它来实现内核是很奇怪的功能内置于他的体内。 可以肯定的是,在load_module函数的文本中,我们很快找到了parse_args调用-看来我们处在正确的轨道上。 接下来,我们快速遍历调用链(一如既往,我们将看到包装器函数和包装器宏,但是我们已经习惯了对开发人员的这种恶作剧视而不见),并且找到了parse_one函数,该函数将所需的参数放在正确的位置。

请注意,没有像人们期望的那样检查参数的有效性,因为内核与模块本身不同,对其用途一无所知。 进行语法检查和数组中元素的数量(是的,可以有一个整数数组作为参数),并且当检测到此类错误时,模块加载将停止,仅此而已。 但是,并不会丢失所有内容,因为在将加载控制权转移到init_module函数之后,该函数可以对设置的参数进行必要的验证,并且如果需要保存抛出 ,则可以终止引导过程。

但是,我们完全忽略了解析函数如何访问参数样本数组的问题,因为没有这个,解析就有些困难。 快速查看代码表明已应用了肮脏的技巧,这是一个明显的技巧-在二进制文件中,find_module_sections函数搜索命名部分__param,将其大小除以记录的大小(还有很多),并通过结构返回必要的数据。 我仍将字母p放在此函数的参数名称之前,但这只是一个问题。

一切似乎都清晰易懂,唯一令人担心的是生成的数据上缺少__initdata属性,初始化后它是否真的可以保留在内存中呢,老实说,可能在常规部分的某个地方对此属性进行了描述,例如,懒洋洋地看,见题词。

总结-周末非常有用,了解L的源代码,记住一些东西并学到一些东西很有趣,但是知识永远不会多余。
好吧,根据我的假设,我没有想到,在L中实施了一个选项,结果是剩余的7%,但这很不明显。

好吧,总而言之,雅罗斯拉夫纳的哭声(怎么可能没有它)为什么我不得不从各种没有官方身份的来源中寻找必要的信息(我不是指内部厨房,而是外部展示),那里有类似本书的文件
“计算机软件。 功能操作系统。
拉斐斯。 系统程序员指南”,还是不再使用?

Source: https://habr.com/ru/post/zh-CN431860/


All Articles