iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 17
1
IoT

Modern Maker : 從那些 Maker 的大小事看 Linux 核心系列 第 17

Day 17:SPI Driver (Part 3) -- 又是一個間諜檔案!

  • 分享至 

  • xImage
  •  

目標:包成一個可以寫的檔案

看起來流程都很類似,都是在 probe 裡面註冊該註冊的東西,然後該管理的資源好。聽起來很簡單吧?吧?吧?

請看今天的幕後花絮!

實作 open

open 的實作跟之前差不多,只是 private_data 換成傳 spi_device

static ssize_t arduino_spi_open(struct inode *inode, struct file *filp)
{
    struct arduino_spi_cdev *arduino = container_of(inode->i_cdev, struct arduino_spi_cdev, cdev);
    if (!arduino) {
    	pr_err("Cannot extrace aduino structure from i_cdev.\n");
	return -EINVAL;
    }
    filp -> private_data = arduino -> spi;
    return 0;
}

實作 write

簡單地說,就是先把 userspace 的東西複製到 kernel 中,然後再用 spi_write 傳出去:

static ssize_t arduino_spi_write(struct file *filp, const char __user *buf, size_t count,
		loff_t *offset)
{
    int err = 0;
    struct spi_device *spi = filp -> private_data;
    if (!spi) {
    	pr_err("Failed to get struct spi_device.\n");
	return -EINVAL;
    }
    char *msg = kzalloc(count + 1, GFP_KERNEL);
    copy_from_user(msg, buf, count);
    err = spi_write(spi, msg, count);
    kfree(msg);
    return err ? err : count;
}

這邊的回傳值沒有做好。因為照理說應該要回傳讀寫的確切值。比較正確的作法應該要從 spi_device 中的 struct spi_statistic 來找出實際傳輸的位元資料。

如果沒有複製到 kernel 會發生什麼事?請看幕後花絮

實作 probe

probe 幾乎跟 I2C 的一樣,只是把對應的東西換成 SPI 的東西:

static int dummy_probe(struct spi_device *spi)
{
    int err = 0;
    pr_info("Dummy device is being probed.\n");

    err = alloc_chrdev_region(&dev, 0, 1, ARDUINO_DEV_NAME);
    if (err < 0) {
        pr_err ("Failed in alloc_chrdev_reion for arduino.\n");
	goto out_alloc_chrdev;
    }

    arduino_class = class_create(THIS_MODULE, ARDUINO_DEV_NAME);
    if (!arduino_class) {
    	pr_err ("Failed to create sysfs class.\n");
	goto out_sysfs_class;
    }

    struct arduino_spi_cdev *arduino = kzalloc(sizeof(struct arduino_spi_cdev), GFP_KERNEL);
    if (!arduino) {
	pr_err("Failed to allocate memory.\n");
    	goto out_oom;
    }
    arduino -> spi = spi;

    cdev_init(&(arduino -> cdev), &arduino_spi_fops);
    arduino->cdev.owner = THIS_MODULE;
    err = cdev_add(&(arduino -> cdev), dev, 1);
    if (err) {
	pr_err("Failed to register cdev.\n");
    	goto out_cdev_add;
    }

    struct device *device = device_create(arduino_class, NULL, dev, NULL, ARDUINO_DEV_NAME);
    if (!device) {
    	pr_err("Failed to create device entry under sysfs.\n");
	goto out_device;
    }
    spi->max_speed_hz = 400000;
    dev_set_drvdata(&(spi->dev), arduino);
    return 0;

out_device:
    cdev_del(&arduino->cdev);
out_cdev_add:
    kfree(arduino);
out_oom:
    class_destroy(arduino_class);
out_sysfs_class:
    unregister_chrdev_region(dev, 1);
out_alloc_chrdev:
    return err;    
}

這時有另外一個問題:SPI 沒有像 i2c_client_setdata 這類的函數。不過可以用 dev_set_drvdata 把他加在 spi->dev 底下,就解決問題了。

實驗

其實跟 I2C 的狀況差不多,只是這次改成使用 python 來簡單測試然後就多了一個幕後花絮。把模組編譯好裝上去之後,打開 Arduino 的序列埠觀察。

Arduino 程式

跟上一篇一模一樣。為了版面簡潔就暫時省略。

Raspberry Pi 程式

這次改用 python:

arduino_spi = open('/dev/arduino', 'w')
#arduino_spi = open('/tmp/test', 'w')
s = input("msg> ")
arduino_spi.write(s)

也滿直接的,就是開那個裝置檔案,然後寫一個訊息。

結果

假定上面的檔案叫做 write_test.py,執行它之後輸入 "This attack from SPI":

$ sudo python3 write_test.py 
msg> This attack from SPI

然後序列埠就出現對應的訊息了:

完整程式

因為完整的程式有點長,所以就把裝置樹、Makefile、Arduino 的程式都放在 gist 上。

幕後花絮

大致上放一些過程中發生的 bug。雖然發生的當下有點怒,但幕後花絮畢竟滿好玩的

就跟你說不要相信 userspace

總之在 I2C 的時候,我忽略了一件事情。根據 Unreliable Guide To Hacking The Linux Kernel

"A pointer into userspace should never be simply dereferenced: data should be copied using these routines. Both return -EFAULT or 0."

一開始是直接 spi_write(spi, buf, count),然後就炸裂了:

[  704.908567] 8<--- cut here ---
[  704.910606] Unable to handle kernel paging request at virtual address 01178fc8
[  704.912688] pgd = e5083e32
[  704.914739] [01178fc8] *pgd=00000000
[  704.916811] Internal error: Oops: 5 [#1] SMP ARM
[  704.918882] Modules linked in: dummy_spi_chrdrv(O) cmac bnep hci_uart btbcm bluetooth ecdh_generic ecc squashfs 8021q garp stp llc binfmt_misc spidev brcmfmac brcmutil sha256_generic libsha256 cfg80211 rfkill i2c_bcm2835 raspberrypi_hwmon snd_bcm2835(C) bcm2835_codec(C) bcm2835_v4l2(C) snd_pcm bcm2835_isp(C) v4l2_mem2mem snd_timer bcm2835_mmal_vchiq(C) videobuf2_dma_contig videobuf2_vmalloc videobuf2_memops videobuf2_v4l2 videobuf2_common snd spi_bcm2835 videodev mc vc_sm_cma(C) uio_pdrv_genirq uio fixed i2c_dev ip_tables x_tables ipv6 nf_defrag_ipv6
[  704.934836] CPU: 1 PID: 178 Comm: spi0 Tainted: G         C O      5.4.59-v7-with-eBPF+ #1
[  704.939535] Hardware name: BCM2835
[  704.941945] PC is at bcm2835_spi_transfer_one+0x304/0xbcc [spi_bcm2835]
[  704.944418] LR is at bcm2835_spi_transfer_one+0x138/0xbcc [spi_bcm2835]
[  704.946799] pc : [<7f086bcc>]    lr : [<7f086a00>]    psr: 20000013
[  704.949178] sp : b3ff3e50  ip : b4d81e80  fp : b3ff3e9c
[  704.951567] r10: 00061a80  r9 : b3d22400  r8 : 7f08a000
[  704.953962] r7 : 80d04f88  r6 : b3caa000  r5 : b1fede30  r4 : b3caa380
[  704.956403] r3 : 01178fc8  r2 : 00000001  r1 : 01178fc9  r0 : 00000001
[  704.958800] Flags: nzCv  IRQs on  FIQs on  Mode SVC_32  ISA ARM  Segment user
[  704.961211] Control: 10c5383d  Table: 31d2006a  DAC: 00000055
[  704.963663] Process spi0 (pid: 178, stack limit = 0x43866c56)
[...]
[  705.037405] Backtrace: 
[  705.039115] [<7f0868c8>] (bcm2835_spi_transfer_one [spi_bcm2835]) from [<80685158>] (spi_transfer_one_message+0x1e4/0x594)
[  705.042559]  r10:ffffe000 r9:b65aa810 r8:b3caa2b8 r7:b3caa000 r6:b1feddd4 r5:b1fede30
[  705.045968]  r4:00000000
[  705.047608] [<80684f74>] (spi_transfer_one_message) from [<80685828>] (__spi_pump_messages+0x320/0x6c0)
[  705.050907]  r10:b65aa810 r9:b65aa810 r8:a0000013 r7:b1feddf8 r6:b1feddd4 r5:b1fedd90
[  705.054185]  r4:b3caa000
[  705.055767] [<80685508>] (__spi_pump_messages) from [<80685be8>] (spi_pump_messages+0x20/0x24)
[  705.058947]  r10:b3faba94 r9:80d04f88 r8:b3caa220 r7:80de79c4 r6:ffffe000 r5:b3caa21c
[  705.062119]  r4:b3caa240
[  705.063645] [<80685bc8>] (spi_pump_messages) from [<80148b70>] (kthread_worker_fn+0xc0/0x210)
[  705.066712] [<80148ab0>] (kthread_worker_fn) from [<80148aac>] (kthread+0x170/0x174)
[  705.069769]  r9:80148ab0 r8:b3caa21c r7:b3ff2000 r6:00000000 r5:b2d612c0 r4:b4ffd980
[  705.072964] [<8014893c>] (kthread) from [<801010ac>] (ret_from_fork+0x14/0x28)
[...]
[  705.090539] ---[ end trace b9f2e801c86990d5 ]---

於是後來把東西先從 userspace 複製過來:

static ssize_t arduino_spi_write(struct file *filp, const char __user *buf, size_t count,
		loff_t *offset)
{
    int err = 0;
    struct spi_device *spi = filp -> private_data;
    if (!spi) {
    	pr_err("Failed to get struct spi_device.\n");
	return -EINVAL;
    }
-   spi_write(spi, msg, count);
+   char *msg = kzalloc(count + 1, GFP_KERNEL);
+   copy_from_user(msg, buf, count);
+   err = spi_write(spi, msg, count);
+   kfree(msg);
-   return 0;
+   return err ? err : count;
}

container_of 亂用

一開始並不是用 dev 底下的 dev_set_drvdata 去管理私有的資料而是亂用 container_of

static int dummy_remove(struct spi_device *spi)
{
    pr_info("Dummy device is removing.\n");
+   struct arduino_spi_cdev *arduino = dev_get_drvdata(&(spi->dev));
-   struct arduino_spi_cdev *arduino = container_of(&spi, struct arduino_spi_cdev, spi);
    device_destroy(arduino_class, dev);
    cdev_del(&(arduino->cdev));
    kfree(arduino);
    class_destroy(arduino_class);
    unregister_chrdev_region(dev, 1);
    return 0;
}

然後就是整個 remove 爆開了。不過因為這個的 oops 太長了,所以就暫時不附上來。

暴走的 python

一開始本來想說用 python 去測試,程式類似這樣:

arduino_spi = open('/dev/arduino', 'w')
s = input("msg> ")
arduino_spi.write(s)

然後就發現一件奇怪的事情:這個狀況之下似乎會不斷地重新傳輸。從 Arduino 序列埠上面的輸出來看,buffer 瞬間變成沙包被塞爆,而且除非 ctrl + C,否則不會停下來:

但如果是一樣的 C 程式:

#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>

int main()
{
    int fd = open("/dev/arduino", O_WRONLY);
    if (fd < 0) {
    	printf("Error: cannot open file.\n");
	return -1;
    }
    char *msg = "b";
    write(fd, msg, strlen(msg));
    close(fd);
}

就只會輸出一次:

ply 去追蹤的話:

kprobe:spi_sync*
{
    @[stack()] = count();
}

會發現更有趣的事情:python 版本的真的是會瘋狂輸出不會停:

ply: active
^Cply: deactivating

@:
{
        spi_sync
        cleanup_module+56272
        __vfs_write+72
        vfs_write+180
        ksys_write+104
        __se_sys_write+24
        __hyp_idmap_text_start
 }: 165788

相較之下,前面 C 語言的程式就真的只會傳輸一次:

ply: active
^Cply: deactivating

@:
{
        spi_sync
        cleanup_module+56272
        __vfs_write+72
        vfs_write+180
        ksys_write+104
        __se_sys_write+24
        __hyp_idmap_text_start
 }: 1

後來發現,關鍵似乎是在 write 的回傳值。在實作中,回傳值設成 0:

static ssize_t arduino_spi_write(struct file *filp, const char __user *buf, size_t count,
		loff_t *offset)
{
    struct spi_device *spi = filp -> private_data;
    if (!spi) {
    	pr_err("Failed to get struct spi_device.\n");
	return -EINVAL;
    }
    char *msg = kzalloc(count + 1, GFP_KERNEL);
    copy_from_user(msg, buf, count);
    spi_write(spi, msg, count);
    kfree(msg);
    return 0;
}

依照 write 系統呼叫的文件,他應該由回傳「實際上讀寫的位元組數目」,或著是對應的錯誤編號的負值。如果把他改成:

static ssize_t arduino_spi_write(struct file *filp, const char __user *buf, size_t count,
		loff_t *offset)
{
+   int err = 0;
    struct spi_device *spi = filp -> private_data;
    if (!spi) {
    	pr_err("Failed to get struct spi_device.\n");
	return -EINVAL;
    }
    char *msg = kzalloc(count + 1, GFP_KERNEL);
    copy_from_user(msg, buf, count);
-   spi_write(spi, msg, count);
+   err = spi_write(spi, msg, count);
    kfree(msg);
-   return 0;
+   return err ? -err : count;
}

再用 python 下去測試,就不會發生了。


上一篇
Day 16:SPI Driver (Part 2) - 傳簡單的訊息!
下一篇
Day 18:spidev - 辣個 userspace 的驅動程式
系列文
Modern Maker : 從那些 Maker 的大小事看 Linux 核心30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言