选择主题
☀️ 浅色
🌙 深色
🌿 护眼
Ctrl+K

Level 3:加一个 native 服务开机自启动

Level 3:加一个 native 服务开机自启动 我想做 在系统里加一个自己的可执行文件,开机自动跑起来,logcat 能看到它的输出。 为什么要先做这个再做驱动?因为它最快让你体验完整的"加代码→配构建→配 init.rc→写 SELinux policy→编译→烧录→验证"闭环。不到你手写驱动的时间的 1/10,但建立的操作习惯后面每个 Level 都在用。 先回答三个问题 Android 系统启动后,用户态的入口是什么? init 进程是所有用户态进程的祖先(PID 1) init 解析 init.rc 文件,按规则启动服务 厂商自己的服务可以通过 init_rc 属性添加到 vendor 分区 vendor 分区和 system 分区是什么关系? system 分区存 Android 通用组件 vendor 分区存厂商(Amlogic/你的公司)的专有部分 Treble 架构强制隔离二者——你的服务应该放 vendor 分区 为什么加了服务还要写 SELinux policy? Android 的 SELinux 默认不让任何未定义的进程运行 不加 .te 文件,你的服务一启动就被 kernel 杀掉 不是 bug,是机制 需要懂的知识 Android.bp(Soong 构建) AOSP 从 Android 7 开始引入 Soong 替代 Makefile,配置文件是 Android.bp。常用模块类型: cc_binary { // 编译为可执行文件 name: "luojService", srcs: ["luojService.cpp"], shared_libs: [ "liblog", ], vendor: true, // 安装到 vendor 分区 } vendor: true 决定了产物的安装路径。 ...

June 9, 2026

Level 2:编译自己的固件并烧录

Level 2:编译自己的固件并烧录 我想做 从源码完整编译出一版固件,烧到板子里,确认跑的是我自己编译的系统。 先回答三个问题 你的项目用什么方式编译? 标准 AOSP:source build/envsetup.sh → lunch → make 本项目(ross):用 build.cfg + ./mk 脚本 两者的区别是什么? 一个固件由哪些东西组成? boot.img(内核 + ramdisk) super.img(system + vendor + product) dtb/dtbo(设备树) 这些镜像文件在 out/target/product/ross/ 下 怎么判断编译成功了? 最后一行:#### build completed successfully #### 关键镜像文件生成且大小合理 烧录后 ro.build.date 是你刚才的编译时间 需要懂的知识 AOSP 构建系统骨架 一个 Android 固件的编译最终是一系列 Makefile 规则的执行,但 AOSP 用了一整套包装: build/envsetup.sh ——设置环境变量,提供 lunch、mmm、make 等命令 lunch ——选择产品+编译类型,本质是设置 TARGET_PRODUCT 和 TARGET_BUILD_VARIANT make ——根据产品配置读取所有 Android.mk/Android.bp,编译出完整系统 在 ross 项目中,build.cfg 代替了 lunch 的手动选择: ...

June 9, 2026

Level 1:确认板子是活的

Level 1:确认板子是活的 我想做 拿到一块 S905X5M 板子,通电,确认它能正常工作,搞清楚"我手里有什么"。 先回答三个问题 板子需要哪些物理连接才能工作? 电源(看板子丝印,12V/5V?DC 口还是接线柱?) 串口(TX/RX/GND 三根线,接 TTL-USB 转接板) 显示(HDMI 接显示器) 网络或 USB(用来 adb 连接) 找出你的板子上每个接口的位置 怎么判断板子"活着"? 串口有输出(bootloader 启动日志) HDMI 有画面(Android 启动动画或桌面) adb 能连上(adb devices 看到设备) 什么是 Android 的板级信息? ro.board.platform——芯片平台代号(S905X5M 是什么?) ro.build.version.release——Android 版本号 ro.build.fingerprint——完整构建指纹(包含厂商/产品/版本/编译号) ro.build.date——固件编译时间 /proc/version——内核版本和编译工具链 这些信息由谁写入的?→ build.prop 文件 需要懂的知识 adb 基础命令 adb(Android Debug Bridge)是 Android 调试的核心工具,三个组件:adb client(你输入命令的终端)、adb server(后台进程)、adbd(板子上跑的守护进程)。只需要记住: adb shell <cmd> ——在板子上执行命令 adb devices ——查看连接的设备列表 adb push/pull ——传文件 Linux 虚拟文件系统 /proc/ ——内核暴露的运行信息(进程、内存、版本),不是真实文件 /sys/ ——内核暴露的设备参数,可以读写来配置驱动 getprop ——读取 Android 系统的属性,底层就是读取 / 下的属性文件 分区概念 ...

June 9, 2026

实操问题记录

实操问题记录 按 target.md 的 Level 对应,只记录实操中遇到的编译/运行错误和异常现象。知识点类内容归属各 Level 文档本身。 Level 3:加一个 native 服务开机自启动 对应学习文档:level-3-native服务.md [L3-01] checkpolicy unrecognized character 现象: ERROR 'unrecognized character' at token ',指向新建的 .te 文件 根因: .te 文件是 Windows CRLF(\r\n)换行符,Linux 的 checkpolicy 不识别 \r 修复: sed -i 's/\r$//' vendor/giec/common/sepolicy/luojService.te 验证: cat -A file.te → 看到 $(LF)而非 ^M$(CRLF) [L3-02] neverallow 违规:dac_override 现象: neverallow check failed ... allow luojService self:capability { dac_override } 根因: dac_override(绕过文件权限)被系统核心策略(domain.te:470)全局禁止,任何 domain 不可申请 修复: 从 .te 中删除 allow luojService self:capability { dac_override }; ...

June 9, 2026

Level 6 总结与扩展:逐函数拆解 GPIO 按键驱动

Level 6 总结与扩展:逐函数拆解 GPIO 按键驱动 本文是 level-6-按键.md 的延伸,以 Amlogic gpio_keypad.c(557 行)为样本,按照代码的物理顺序,逐函数解释它干了什么。 读完本文你会:能自己读懂一个陌生驱动,知道每个函数在做什么。 驱动源文件:common/common_drivers/drivers/input/keyboard/gpio_keypad.c 代码总览(完整源码) 以下是 gpio_keypad.c 的完整代码。每个函数后面跟着逐行解释。 // SPDX-License-Identifier: (GPL-2.0+ OR MIT) /* * Copyright (c) 2019 Amlogic, Inc. All rights reserved. */ #include <linux/module.h> #include <linux/init.h> #include <linux/types.h> #include <linux/string.h> #include <linux/kstrtox.h> #include <linux/input.h> #include <linux/platform_device.h> #include <linux/of.h> #include <linux/gpio/consumer.h> #include <linux/interrupt.h> #include <linux/slab.h> #include <linux/amlogic/pm.h> #include <linux/irq.h> #include <linux/amlogic/power_domain.h> #include <linux/amlogic/gpiolib.h> #define DEFAULT_SCAN_PERION 20 #define DEFAULT_POLL_MODE 0 #define KEY_JITTER_COUNT 1 struct pin_desc { int current_status; struct gpio_desc *desc; int irq_num; u32 code; u32 key_type; const char *name; int count; bool ignore; }; struct gpio_keypad { int key_size; int use_irq;/* 1:irq mode ; 0:polling mode */ int scan_period; struct pin_desc *key; struct pin_desc *current_key; struct timer_list polling_timer; struct input_dev *input_dev; struct class kp_class; struct class resetkey_class; unsigned int reset_count; }; static irqreturn_t gpio_irq_handler(int irq, void *data) { struct gpio_keypad *keypad; keypad = (struct gpio_keypad *)data; mod_timer(&keypad->polling_timer, jiffies + msecs_to_jiffies(20)); return IRQ_HANDLED; } static void report_key_code(struct gpio_keypad *keypad, int gpio_val) { struct pin_desc *key = keypad->current_key; if (key->count >= KEY_JITTER_COUNT) { key->current_status = gpio_val; if (key->current_status) { input_event(keypad->input_dev, key->key_type, key->code, 0); dev_dbg(&keypad->input_dev->dev, "key %d up.\n", key->code); } else { input_event(keypad->input_dev, key->key_type, key->code, 1); // Count key press if (strcmp(key->name, "bluetooth") == 0) { keypad->reset_count++; } dev_dbg(&keypad->input_dev->dev, "key %d down.\n", key->code); } input_sync(keypad->input_dev); key->count = 0; } } static void polling_timer_handler(struct timer_list *t) { int i; int gpio_val; int is_pressing = 0; struct gpio_keypad *keypad = from_timer(keypad, t, polling_timer); if (keypad->use_irq) {/* irq mode */ for (i = 0; i < keypad->key_size; i++) { gpio_val = gpiod_get_value(keypad->key[i].desc); gpio_val |= keypad->key[i].ignore; if (keypad->key[i].current_status != gpio_val) { keypad->key[i].count++; keypad->current_key = &keypad->key[i]; report_key_code(keypad, gpio_val); } else { keypad->key[i].count = 0; } if (gpio_val == 0 || keypad->key[i].current_status == 0) is_pressing = 1; } if (is_pressing) mod_timer(&keypad->polling_timer, jiffies + msecs_to_jiffies(keypad->scan_period)); } else {/* polling mode */ for (i = 0; i < keypad->key_size; i++) { gpio_val = gpiod_get_value(keypad->key[i].desc); gpio_val |= keypad->key[i].ignore; if (keypad->key[i].current_status != gpio_val) { keypad->key[i].count++; keypad->current_key = &keypad->key[i]; report_key_code(keypad, gpio_val); } else { keypad->key[i].count = 0; } } mod_timer(&keypad->polling_timer, jiffies + msecs_to_jiffies(keypad->scan_period)); } } static ssize_t table_show(struct class *cls, struct class_attribute *attr, char *buf) { struct gpio_keypad *keypad = container_of(cls, struct gpio_keypad, kp_class); int i; int len = 0; for (i = 0; i < keypad->key_size; i++) { len += sprintf(buf + len, "[%d]: name = %-21s status = %-5d\n", i, keypad->key[i].name, keypad->key[i].current_status); } return len; } static ssize_t ignore_show(struct class *cls, struct class_attribute *attr, char *buf) { struct gpio_keypad *keypad = container_of(cls, struct gpio_keypad, kp_class); int index; int len = 0; for (index = 0; index < keypad->key_size; index++) { len += sysfs_emit_at(buf, len, "%s,%d: %c\n", keypad->key[index].name, keypad->key[index].code, keypad->key[index].ignore ? 'Y' : 'N'); } return len; } static ssize_t ignore_store(struct class *cls, struct class_attribute *attr, const char *buf, size_t count) { struct gpio_keypad *keypad = container_of(cls, struct gpio_keypad, kp_class); struct device *dev = keypad->input_dev->dev.parent; char nbuf[128]; char tmp[32]; int index; char *value = nbuf; char *name; bool ignore; bool found; if (count > sizeof(nbuf)) { dev_err(dev, "data is too long\n"); return -EINVAL; } memcpy(nbuf, buf, count); strreplace(nbuf, '\n', '\0'); name = strsep(&value, " "); if (!name) goto err; if (!strcmp("all", name)) { for (index = 0; index < keypad->key_size; index++) keypad->key[index].ignore = true; dev_dbg(dev, "ignore all keys\n"); } else if (!strcmp("none", name)) { for (index = 0; index < keypad->key_size; index++) keypad->key[index].ignore = false; dev_dbg(dev, "enable all keys\n"); } else { if (!value) goto err; if (kstrtobool(value, &ignore)) { dev_err(dev, "invalid '%s', please use Y/N instead\n", value); return -EINVAL; } /* search key */ found = false; for (index = 0; index < keypad->key_size; index++) { snprintf(tmp, sizeof(tmp), "%s,%d", keypad->key[index].name, keypad->key[index].code); if (!strcmp(tmp, name)) { found = true; break; } } if (!found) { dev_err(dev, "invalid key '%s'\n", name); return -EINVAL; } keypad->key[index].ignore = ignore; dev_dbg(dev, "ignore %s: %s\n", name, value); } return count; err: dev_err(dev, "the correct format is: [name],[code] [ignore]\n"); return -EINVAL; } static CLASS_ATTR_RO(table); static CLASS_ATTR_RW(ignore); static struct attribute *meson_gpiokey_attrs[] = { &class_attr_table.attr, &class_attr_ignore.attr, NULL }; ATTRIBUTE_GROUPS(meson_gpiokey); // Add show function for reset count static ssize_t count_show(struct class *cls, struct class_attribute *attr, char *buf) { struct gpio_keypad *keypad = container_of(cls, struct gpio_keypad, resetkey_class); return sprintf(buf, "%u\n", keypad->reset_count); } static CLASS_ATTR_RO(count); static struct attribute *resetkey_attrs[] = { &class_attr_count.attr, NULL }; ATTRIBUTE_GROUPS(resetkey); static int meson_gpio_kp_probe(struct platform_device *pdev) { struct gpio_desc *desc; int ret, i; struct input_dev *input_dev; struct gpio_keypad *keypad; struct irq_desc *d; unsigned int number; int wakeup_source = 0; int register_flag = 0; if (!(pdev->dev.of_node)) { dev_err(&pdev->dev, "pdev->dev.of_node == NULL!\n"); return -EINVAL; } keypad = devm_kzalloc(&pdev->dev, sizeof(struct gpio_keypad), GFP_KERNEL); if (!keypad) return -EINVAL; ret = of_property_read_u32(pdev->dev.of_node, "detect_mode", &keypad->use_irq); if (ret) /* The default mode is polling. */ keypad->use_irq = DEFAULT_POLL_MODE; ret = of_property_read_u32(pdev->dev.of_node, "scan_period", &keypad->scan_period); if (ret) /* he default scan period is 20. */ keypad->scan_period = DEFAULT_SCAN_PERION; if (of_property_read_bool(pdev->dev.of_node, "wakeup-source")) wakeup_source = 1; ret = of_property_read_u32(pdev->dev.of_node, "key_num", &keypad->key_size); if (ret) { dev_err(&pdev->dev, "failed to get key_num!\n"); return -EINVAL; } keypad->key = devm_kzalloc(&pdev->dev, (keypad->key_size) * sizeof(*keypad->key), GFP_KERNEL); if (!(keypad->key)) return -EINVAL; for (i = 0; i < keypad->key_size; i++) { /* get all gpio desc. */ desc = devm_gpiod_get_index(&pdev->dev, "key", i, GPIOD_IN); if (IS_ERR_OR_NULL(desc)) return -EINVAL; keypad->key[i].desc = desc; /* The gpio default is high level. */ keypad->key[i].current_status = 1; ret = of_property_read_u32_index(pdev->dev.of_node, "key_code", i, &keypad->key[i].code); if (ret < 0) { dev_err(&pdev->dev, "find key_code=%d finished\n", i); return -EINVAL; } ret = of_property_read_u32_index(pdev->dev.of_node, "key_type", i, &keypad->key[i].key_type); if (ret) keypad->key[i].key_type = EV_KEY; ret = of_property_read_string_index(pdev->dev.of_node, "key_name", i, &keypad->key[i].name); if (ret < 0) { dev_err(&pdev->dev, "find key_name=%d finished\n", i); return -EINVAL; } gpiod_direction_input(keypad->key[i].desc); gpiod_set_pull(keypad->key[i].desc, GPIOD_PULL_UP); } keypad->kp_class.name = "gpio_keypad"; keypad->kp_class.owner = THIS_MODULE; keypad->kp_class.class_groups = meson_gpiokey_groups; ret = class_register(&keypad->kp_class); if (ret) { dev_err(&pdev->dev, "fail to create gpio keypad class.\n"); return -EINVAL; } // After registering kp_class, add resetkey class registration keypad->resetkey_class.name = "resetkey"; keypad->resetkey_class.owner = THIS_MODULE; keypad->resetkey_class.class_groups = resetkey_groups; keypad->reset_count = 0; ret = class_register(&keypad->resetkey_class); if (ret) { dev_err(&pdev->dev, "fail to create resetkey class.\n"); class_unregister(&keypad->kp_class); return -EINVAL; } /* input */ input_dev = input_allocate_device(); if (!input_dev) return -EINVAL; for (i = 0; i < keypad->key_size; i++) { input_set_capability(input_dev, keypad->key[i].key_type, keypad->key[i].code); dev_dbg(&pdev->dev, "%s key(%d) type(0x%x) registered.\n", keypad->key[i].name, keypad->key[i].code, keypad->key[i].key_type); } input_dev->name = "gpio_keypad"; input_dev->phys = "gpio_keypad/input0"; input_dev->dev.parent = &pdev->dev; input_dev->id.bustype = BUS_ISA; input_dev->id.vendor = 0x0001; input_dev->id.product = 0x0001; input_dev->id.version = 0x0100; input_dev->rep[REP_DELAY] = 0xffffffff; input_dev->rep[REP_PERIOD] = 0xffffffff; input_dev->keycodesize = sizeof(unsigned short); input_dev->keycodemax = 0x1ff; keypad->input_dev = input_dev; ret = input_register_device(keypad->input_dev); if (ret < 0) { input_free_device(keypad->input_dev); return -EINVAL; } platform_set_drvdata(pdev, keypad); timer_setup(&keypad->polling_timer, polling_timer_handler, 0); if (keypad->use_irq) { for (i = 0; i < keypad->key_size; i++) { keypad->key[i].count = 0; keypad->key[i].irq_num = gpiod_to_irq(keypad->key[i].desc); ret = devm_request_irq(&pdev->dev, keypad->key[i].irq_num, gpio_irq_handler, IRQF_TRIGGER_FALLING, "gpio_keypad", keypad); if (ret) { dev_err(&pdev->dev, "Requesting irq failed!\n"); input_free_device(keypad->input_dev); del_timer(&keypad->polling_timer); return -EINVAL; } if (keypad->key[i].code == KEY_POWER) { d = irq_to_desc(keypad->key[i].irq_num); if (d) { number = d->irq_data.parent_data->hwirq - 32; pwr_ctrl_irq_set(number, 1, 0); register_flag = 1; } } } } else { mod_timer(&keypad->polling_timer, jiffies + msecs_to_jiffies(keypad->scan_period)); } if (wakeup_source) { if (register_flag) dev_dbg(&pdev->dev, "succeed to register wakeup source!\n"); else dev_err(&pdev->dev, "failed to register wakeup source!\n"); } return 0; } static int meson_gpio_kp_remove(struct platform_device *pdev) { struct gpio_keypad *keypad; keypad = platform_get_drvdata(pdev); class_unregister(&keypad->resetkey_class); class_unregister(&keypad->kp_class); input_unregister_device(keypad->input_dev); input_free_device(keypad->input_dev); del_timer(&keypad->polling_timer); return 0; } static const struct of_device_id key_dt_match[] = { { .compatible = "amlogic, gpio_keypad", }, {} }; static int meson_gpio_kp_suspend(struct device *dev) { struct gpio_keypad *pdata; pdata = (struct gpio_keypad *)dev_get_drvdata(dev); if (!pdata->use_irq) del_timer(&pdata->polling_timer); return 0; } static int meson_gpio_kp_resume(struct device *dev) { int i; struct gpio_keypad *pdata; pdata = (struct gpio_keypad *)dev_get_drvdata(dev); if (!pdata->use_irq) mod_timer(&pdata->polling_timer, jiffies + msecs_to_jiffies(5)); if (get_resume_method() == POWER_KEY_WAKEUP) { for (i = 0; i < pdata->key_size; i++) { if (pdata->key[i].code == KEY_POWER) { dev_dbg(dev, "gpio keypad wakeup\n"); input_report_key(pdata->input_dev, KEY_POWER, 1); input_sync(pdata->input_dev); input_report_key(pdata->input_dev, KEY_POWER, 0); input_sync(pdata->input_dev); break; } } } return 0; } static int meson_gpio_kp_restore(struct device *dev) { struct gpio_keypad *keypad = dev_get_drvdata(dev); struct gpio_desc *desc; int index; for (index = 0; index < keypad->key_size; index++) { desc = devm_gpiod_get_index(dev, "key", index, GPIOD_IN); if (IS_ERR_OR_NULL(desc)) { dev_err(dev, "failed to request to gpio while restore\n"); return -EINVAL; } gpiod_direction_input(desc); gpiod_set_pull(desc, GPIOD_PULL_UP); keypad->key[index].desc = desc; } return meson_gpio_kp_resume(dev); } static int meson_gpio_kp_freeze(struct device *dev) { struct gpio_keypad *keypad = dev_get_drvdata(dev); int index; int ret; ret = meson_gpio_kp_suspend(dev); if (ret) return ret; for (index = 0; index < keypad->key_size; index++) devm_gpiod_put(dev, keypad->key[index].desc); return 0; } static const struct dev_pm_ops meson_gpio_kp_pm_ops = { .suspend = meson_gpio_kp_suspend, .resume = meson_gpio_kp_resume, .freeze = meson_gpio_kp_freeze, .thaw = meson_gpio_kp_resume, .poweroff = meson_gpio_kp_suspend, .restore = meson_gpio_kp_restore, }; static struct platform_driver meson_gpio_kp_driver = { .probe = meson_gpio_kp_probe, .remove = meson_gpio_kp_remove, .driver = { .name = "gpio-keypad", .pm = &meson_gpio_kp_pm_ops, .of_match_table = key_dt_match, }, }; int __init meson_gpio_kp_init(void) { return platform_driver_register(&meson_gpio_kp_driver); } void __exit meson_gpio_kp_exit(void) { platform_driver_unregister(&meson_gpio_kp_driver); } 逐函数解释 以下按代码物理顺序,逐个函数解释它干了什么。 ...

June 9, 2026

Level 5 扩展与总结:GPIO 子系统深度分析

Level 5 扩展与总结:GPIO 子系统深度分析 本文是 level-5-GPIO.md 的延伸,记录从编写 fun_led.c 到发现 leds-gpio 现成方案过程中,对 GPIO 子系统的深度分析。 1. 驱动分析:硬编码 GPIO 的问题 当前 fun_led.c 采用的是硬编码 GPIO 编号方式: #define LED_GPIO 28 /* 写死在代码里的 GPIO 编号 */ ... gpio_request(28, "fun_led"); gpio_set_value(28, val); 硬编码带来的三个风险 风险 1:内核升级,编号体系变化 LED_GPIO 28 对应 S7D 的 GPIOD_4。这个 28 来自 meson-s7d-gpio.h 中 pins 数组的排列顺序。如果 Amlogic 在新内核 BSP 中调整了 pins 数组顺序(例如把 E 组排到 D 组之后),GPIOD_4 的编号就不再是 28 了。驱动里写死的 28 会指向错误的 pin。 风险 2:换板子不可用 同一份 fun_led.c 源码,想在另一个 S905 型号(比如 S6、S4D)上使用。S6 的 GPIOD_4 = 4,S4D 的 GPIOD_4 可能又是另一个编号。驱动里写着 28,换板子必炸。 ...

June 9, 2026

Level 5:操作 GPIO

Level 5:操作 GPIO 我想做 控制板子上的一个物理引脚,让它输出高/低电平,用万用表或 LED 确认电平变化。 先回答三个问题 GPIO 在系统里是以什么方式呈现的? 物理上:芯片的一个管脚,可以设置输出高/低电平或读取输入电平 代码上:GPIO controller 是一个硬件模块,通过寄存器操作 给用户的接口:sysfs 或 libgpiod,或者自己写驱动操作寄存器 设备树(DTS)和驱动是什么关系? DTS 描述硬件有什么、接在哪个总线、地址/中断号是什么 驱动根据 DTS 的信息来初始化硬件 同一个驱动可以支持多个 DTS 描述的实例 你的板子的 GPIO 信息从哪儿来? 原理图(PDF)——告诉你在板子上哪个排针/接口引出了哪个 GPIO DTS——告诉内核哪个 GPIO controller 对应哪个基地址 datasheet——详细到每个寄存器的位定义 需要懂的知识 GPIO 子系统 内核用 gpiolib 统一管理 GPIO。用户态访问 GPIO 有两种方式: 旧方式(sysfs,kernel < 5.10): echo 100 > /sys/class/gpio/export echo out > /sys/class/gpio/gpio100/direction echo 1 > /sys/class/gpio/gpio100/value 新方式(libgpiod,kernel >= 5.10): gpioinfo gpioset gpiochip0 100=1 你的内核是 5.15,优先用 libgpiod。 ioremap 的作用 ...

June 9, 2026

Level 4 总结与扩展

Level 4 总结与扩展 总结:一个字符驱动的完整生命周期 Level 4 的核心命题——“写一个内核模块,用户态能读能写”——拆解为三层:内核侧(驱动代码)→ 构建侧(编译系统)→ 运行侧(板子验证)。下面逐层复盘,标注优先级。 第一层:内核侧 —— 驱动代码本身 这层是跨平台通用的 Linux 驱动知识,与 Android 构建系统无关。 🔴 核心必懂 知识点 一句话 关联的坑 file_operations 驱动的"函数表"——把用户态的系统调用(open/read/write)映射成驱动函数 多态机制,所有字符驱动公用 register_chrdev(0, name, &fops) 向内核注册字符设备,参数 0 让内核自动分配主设备号 返回值是主设备号,mknod 需要它 copy_from_user / copy_to_user 内核与用户空间之间的安全数据拷贝 不能直接用 memcpy——见下方说明 dev_read 必须返回 0 表示 EOF VFS 规范:当数据读完时必须返回 0,否则 cat 等工具无限循环 L4-05 bug:忘记返回 0,cat 死循环 module_init / module_exit 模块加载/卸载的入口和出口 卸载时未 unregister_chrdev 会导致空悬设备节点 为什么不能用 memcpy 替代 copy_to_user? 三个原因: 安全校验:copy_to_user 会检查用户态指针是否在当前进程的地址空间内,防止内核被伪造指针攻击 缺页处理:用户态内存可能被换出(swap),copy_to_user 能触发缺页中断把页面换回来,memcpy 做不到 SMAP/PAN 硬件保护:ARMv8.1+ 有 PAN(Privileged Access Never)特性,内核态默认禁止直接访问用户态内存,copy_to_user 会临时关闭此保护 VFS 调用链路(用户态 open("/dev/simple_char") → 驱动 dev_open()): ...

June 9, 2026

Level 4:写一个字符设备驱动

Level 4:写一个字符设备驱动 我想做 在内核里加一个最小内核模块,用户态 echo "hi" > /dev/simple_char 能写进去,cat /dev/simple_char 能读出来。 先回答三个问题 内核驱动和用户态程序的根本区别是什么? 驱动运行在内核态,可以访问所有硬件资源和内存 用户态程序通过系统调用(open/read/write/ioctl)请求内核代为操作 内核态出错 → 整个系统崩溃(kernel panic);用户态出错 → 只崩那个进程 Linux 设备驱动分哪几类? 字符设备──像文件一样 open/read/write/close,顺序访问。例子:串口(/dev/ttyAML0)、GPIO、I2C、SPI、按键。你的板子上 ls -l /dev/ 看到前面是 c 的都是字符设备 块设备──按块随机读写,有缓存层。例子:eMMC(/dev/block/mmcblk0)、U 盘、SD 卡。前面是 b 网络设备──通过 socket 接口访问,不通过文件系统。例子:以太网(eth0)、WiFi(wlan0)。ip link show 看到的就是 你的板子上跑 adb shell ls -l /dev/ | head -20,看看哪些是字符设备(c)、哪些是块设备(b) 什么是 file_operations? 它是一个结构体,里面全是函数指针 每个字符驱动需要告诉内核:“当用户调用 open() 时,执行我这个函数;调用 read() 时,执行我那个函数” 这是一个多态机制——不同的驱动实现相同的接口,用户态不用关心底层实现 需要懂的知识 驱动模块的骨架 每个内核模块都有两个必经的函数: module_init(mod_init); // insmod 时调用 module_exit(mod_exit); // rmmod 时调用 __init 和 __exit 标记:这些函数在运行完后可以释放内存。 ...

June 9, 2026

Level 9:修复一个 SELinux 权限问题

Level 9:修复一个 SELinux 权限问题 我想做 当一个服务因为 SELinux 权限被拒绝而启动失败时,能独立定位并修复。这是 Android 嵌入式开发中最常见的调试场景。 先回答三个问题 SELinux 在 Android 上到底保护什么? 没有 SELinux:一个进程拿到 root 就可以做任何事(读所有文件、杀其他进程、控制硬件) 有 SELinux:每个进程被限制在最小的权限域中(principle of least privilege) 即使进程是 root 用户,SELinux 仍然会拒绝未经授权的操作 avc: denied = SELinux 告诉你"这个操作不在你的权限范围内" domain 和 type 是什么关系? domain 是进程的标签(你是什么) type 是文件/资源的标签(这是什么) allow domain type:class { permission } = “谁 对什么 做什么” audit2allow 的原理? 从 dmesg 或 /proc/avc 中读取 avc denied 日志 自动解析出需要的 allow 语句 但注意:audit2allow 只是工具,直接套用可能给过多权限,要人工审查 需要懂的知识 avc denied 日志解读 ...

June 9, 2026