研究研究看似简单的switch问题

深入解析C语言switch语句的实现机制及原因

这个问题,之前一直没有去思考,在c和指针这本书中,在谈到switch是给出了这样一段话:
这里写图片描述
也就是说switch的条件要是整型值?好像一直这么用,却没有研究过为什么。惭愧。。。
研究c语言最好就是看其汇编实现,下面在linux 64位上实验。
这里写图片描述
编译运行,输出10;
然后将目标文件dump出来

int main()
{
  40052d:   55                      push   %rbp
  40052e:   48 89 e5                mov    %rsp,%rbp
  400531:   48 83 ec 10             sub    $0x10,%rsp
int a=10;
  400535:   c7 45 fc 0a 00 00 00    movl   $0xa,-0x4(%rbp)
switch(a)
  40053c:   8b 45 fc                mov    -0x4(%rbp),%eax
  40053f:   83 f8 01                cmp    $0x1,%eax
  400542:   74 07                   je     40054b <main+0x1e>
  400544:   83 f8 0a                cmp    $0xa,%eax
  400547:   74 0e                   je     400557 <main+0x2a>
  400549:   eb 18                   jmp    400563 <main+0x36>
{
case 1: 
printf("1\n");
  40054b:   bf 04 06 40 00          mov    $0x400604,%edi
  400550:   e8 bb fe ff ff          callq  400410 <puts@plt>
break;
  400555:   eb 17                   jmp    40056e <main+0x41>
case 10:
printf("10\n");
  400557:   bf 06 06 40 00          mov    $0x400606,%edi
  40055c:   e8 af fe ff ff          callq  400410 <puts@plt>
break;
  400561:   eb 0b                   jmp    40056e <main+0x41>
default:
printf("default\n");
  400563:   bf 09 06 40 00          mov    $0x400609,%edi
  400568:   e8 a3 fe ff ff          callq  400410 <puts@plt>
break;
  40056d:   90                      nop
}
return 0;
  40056e:   b8 00 00 00 00          mov    $0x0,%eax
}

主要看看这段代码,就是switch一句代码展开好多汇编代码

switch(a)
  40053c:   8b 45 fc                mov    -0x4(%rbp),%eax
  40053f:   83 f8 01                cmp    $0x1,%eax
  400542:   74 07                   je     40054b <main+0x1e>
  400544:   83 f8 0a                cmp    $0xa,%eax
  400547:   74 0e                   je     400557 <main+0x2a>
  400549:   eb 18                   jmp    400563 <main+0x36>

将a的值存放到寄存器eax中,然后出现两个比较三个挑战,就是下面的两个case 条件,
是否可以就此下结论,switch的实现如上所述,翻开深入理解计算机系统,有一节讲的switch,而上面说的是switch的实现是利用跳转表,而我的测试代码却没有。
抛开这个问题不谈,使用比较跳转这样的实现方式是不是有点效率偏低。如果有n多个case比较,那不是死定了。。。而深入计算机书中讲的的是一种查表法(好吧,这让我想起了FPGA芯片的实现方式)。
是不是gcc会根据case多少自己优化?
接下来将case加多,编写实验,将打印语句去掉。
这里写图片描述
然后dump出目标文件,如下

int main()
{
  4004ed:   55                   push   %rbp
  4004ee:   48 89 e5             mov    %rsp,%rbp
 int a=10;
  4004f1:   c7 45 fc 0a 00 00 00 movl   $0xa,-0x4(%rbp)
 switch(a)
  4004f8:   8b 45 fc             mov    -0x4(%rbp),%eax
  4004fb:   83 e8 0a             sub    $0xa,%eax
  4004fe:   83 f8 07             cmp    $0x7,%eax
  400501:   77 0c                   ja     40050f <main+0x22>
  400503:   89 c0                   mov    %eax,%eax
  400505:   48 8b 04 c5 a8 05 40 mov    0x4005a8(,%rax,8),%rax
  40050c:   00 
  40050d:   ff e0                   jmpq   *%rax
  case 16:
   break;
  case 17:
   break;
  default:
   break;
  40050f:   90                   nop
  400510:   eb 01                   jmp    400513 <main+0x26>
  case 15:
   break;
  case 16:
   break;
  case 17:
   break;
  400512:   90                   nop
  default:
   break;
 }
 return 0;
  400513:   b8 00 00 00 00       mov    $0x0,%eax
}

令人发指的事情出现了。。看看这段代码

switch(a)
  4004f8:   8b 45 fc             mov    -0x4(%rbp),%eax
  4004fb:   83 e8 0a             sub    $0xa,%eax
  4004fe:   83 f8 07             cmp    $0x7,%eax
  400501:   77 0c                   ja     40050f <main+0x22>
  400503:   89 c0                   mov    %eax,%eax
  400505:   48 8b 04 c5 a8 05 40 mov    0x4005a8(,%rax,8),%rax
  40050c:   00
  40050d:   ff e0                   jmpq   *%rax

并没有出现依次比较跳转的实现方式。
f8处代码,将a的值存放到寄存器eax中
fb代码,将eax的值减去10;实际这是一种映射思想,
下面一个比较和跳转是判断eax的值超过7的,对应default的情况。
再下面的mov和jmpq就是重要的问题了。
看看这个汇编代码
mov 0x4005a8(,%rax,8),%rax
看看这句代码有一种似曾相识的感觉呀。

脑洞大开中。。。。。
bingo
在linux v0.12 源码中看到过这个语法,在研究系统函数时看到的。
那篇博客链接为http://blog.youkuaiyun.com/u010442328/article/details/46812049
其地址计算为0x4005a8+rax×8,前面面是数组的基地址,rax为数组的下标,8是每个数组元素占的字节数。

原来switch是这样实现的。
那么问题来了,switch条件为什么只能是整型值?
什么是整型值?

从switch实现的机制上来说,判断条件是作为数组的下标的,用于查表的,这一点上是满足判断条件必须为整数的。是不是还有什么深层次的原因呢?

看看 gcc编译器会有什么答案,看一段代码

int main()
{
 switch("abc")
 {
  case "abc":
   break;
  default:
   break;
 }

 return 0;
}

gcc编译为
这里写图片描述
从编译结果看并没有什么更深入的线索,
根据gcc给出的error,谷歌吧
在stackoverflow中也有人研究这个问题。
链接http://stackoverflow.com/questions/650162/why-switch-statement-cannot-be-applied-on-strings
其中一个解释为
The reason why has to do with the type system. C/C++ doesn’t really support strings as a type. It does support the idea of a constant char array but it doesn’t really fully understand the notion of a string.
In order to generate the code for a switch statement the compiler must understand what it means for two values to be equal. For items like ints and enums, this is a trivial bit comparison. But how should the compiler compare 2 string values? Case sensitive, insensitive, culture aware, etc … Without a full awareness of a string this cannot be accurately answered.
Additionally, C/C++ switch statements are typically generated as branch tables. It’s not nearly as easy to generate a branch table for a string style switch.
在其他语言中,是有支持的非整型值的switch,但是在c中并没有支持。这个问题保留。

<think> 首先我需要理解用户的问题。用户询问在不开启CONFIG_BLK_DEV_INITRD的情况下使用tmpfs导致的三个问题:内核镜像变大、无法挂载加密文件系统、以及hi_clk.ko内置后GPIO异常。这些问题看似独立,但其实都与内核启动流程和驱动初始化顺序有关。 第一个问题关于内核镜像变大,这很明显是因为用户把所有驱动都built-in到内核了。原本这些驱动可以作为模块放在initramfs里,现在为了不使用initramfs就全部内置了。特别是tmpfs相关的驱动如fuse、crypto等,这些模块本身就比较大,内置后自然会让内核镜像膨胀。不过这个问题相对好解决,只需要在配置内核时仔细筛选,只保留绝对必要的驱动内置即可。 第二个挂载加密文件系统失败的问题比较复杂。关键点在于加密文件系统需要内核在挂载rootfs前就加载解密模块。在使用initramfs时,这些模块可以放在initramfs里提前加载。但现在用户没用initramfs,解密模块要么还没加载,要么加载顺序不对。我注意到用户提到tmpfs,可能想用它作为rootfs的过渡方案?但tmpfs本身无法持久化,而且加载时机也很关键。解决方法可以从两个方向考虑:要么用更早的加载机制如earlycpio,要么把所有必要模块内置并处理好依赖顺序。 第三个GPIO异常问题很有意思。用户发现hi_clk.ko内置后出现问题,这明显是驱动初始化顺序的问题。clock驱动属于底层基础驱动,必须在GPIO驱动之前初始化。当作为模块时,initramfs会按依赖顺序加载;而built-in后,内核的initcall机制可能没有正确处理这个顺序。特别是hi_clk可能使用了较晚的initcall级别,导致比GPIO驱动初始化晚。解决方法需要在内核源码中手动调整驱动的initcall级别,把hi_clk的级别调高(数字调小),确保它先于GPIO驱动初始化。 综合这三个问题,根源都在于避开initramfs后如何重构驱动加载机制。用户可能需要权衡:要么接受较大的内核镜像并仔细调整驱动顺序,要么考虑其他替代initramfs的方案。比如可以研究下是否能用CONFIG_INITRAMFS_SOURCE配合cpio,或者利用某些架构特定的early boot机制。 </think> 以下是针对三个问题的原因分析及解决方案: ### 1. 内核镜像变大的原因及解决 **原因**: 未启用 `CONFIG_BLK_DEV_INITRD` 时,无法通过 initramfs 动态加载模块。所有必需驱动(如 tmpfs 依赖的 `fuse.ko`、`crypto.ko` 等)需编译进内核(`y`),导致内核镜像体积膨胀。例如: ```c // 内核配置示例 (部分) CONFIG_TMPFS=y // tmpfs 核心支持 CONFIG_CRYPTO_USER=y // 加密算法支持 CONFIG_FUSE_FS=y // FUSE 文件系统支持 ``` **解决**: - **精简配置**:仅将关键驱动设为 `built-in`,非核心模块设为 `m`(需确保有加载路径)。 - **替代方案**: 使用 `CONFIG_INITRAMFS_SOURCE` 指定 CPIO 镜像路径(无需 `CONFIG_BLK_DEV_INITRD`): ```c CONFIG_INITRAMFS_SOURCE="/path/to/tiny_initramfs.cpio" ``` --- ### 2. 无法挂载加密文件系统的原因及解决 **原因**: - 加密文件系统(如 `dm-crypt`)需在内核挂载 rootfs **前**加载解密模块。 - 未使用 initramfs 时,内核无法在早期阶段访问加密驱动(如 `cryptd.ko`),导致挂载失败。 **解决**: - **方案一**:将加密驱动编译进内核(需权衡体积): ```c CONFIG_DM_CRYPT=y CONFIG_CRYPTO_AES=y CONFIG_CRYPTO_XTS=y ``` - **方案二**:使用 **Early Userspace** 机制: 1. 创建微型 initramfs(仅含 `/init` 脚本和必需模块) 2. 在 `/init` 中加载加密模块后挂载真实 rootfs: ```bash #!/bin/sh mount -t proc proc /proc insmod /lib/modules/cryptd.ko # 加载关键模块 mount /dev/mapper/encrypted_root /new_root exec switch_root /new_root /sbin/init ``` --- ### 3. hi_clk.ko 导致 GPIO 故障的原因及解决 **原因**: 时钟驱动 `hi_clk.ko` 的初始化晚于 GPIO 驱动依赖的时钟源初始化。作为模块时,加载顺序可控: ```bash # 正确顺序:先加载时钟驱动 insmod hi_clk.ko insmod gpio_driver.ko ``` 但编译进内核后,依赖关系未明确指定: - **GPIO 驱动**:在 `arch_initcall` 阶段初始化(较早) - **hi_clk 驱动**:在 `device_initcall` 阶段初始化(较晚) 导致 GPIO 初始化时无法获取时钟资源。 **解决**: 在内核源码中调整驱动初始化优先级: ```c // 修改 hi_clk 驱动的初始化级别(drivers/clk/hisilicon/clk-hiXXXX.c) -postcore_initcall(hi_clk_init); // 原优先级 +subsys_initcall(hi_clk_init); // 提高优先级(数字更小) ``` **优先级顺序参考**(数字越小越早): 1. `pure_initcall` (0) 2. `core_initcall` (1) 3. `postcore_initcall` (2) 4. `arch_initcall` (3) // GPIO 通常在此级 5. `subsys_initcall` (4) // 建议 hi_clk 设在此级 6. `fs_initcall` (5) 7. `device_initcall` (6) // hi_clk 默认可能在此 --- ### 总结建议 - **优先尝试 Early Userspace**:用小 initramfs 解决模块加载和加密问题。 - **若坚持无 initramfs**: ⑴ 精简内置驱动,仅保留关键项(如 `tmpfs` + `crypto`) ⑵ 调整驱动初始化顺序(参考第3点) ⑶ 验证时钟和 GPIO 的 probe 顺序:`dmesg | grep "clk\|gpio"`
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值