作者介绍:
陈松,毕业于天津大学精密仪器专业。先后在索尼爱立信,意法爱立信和华为技术有限公司任软件工程师,软件架构师等职务,主要负责手机内核和应用的开发工作。2019年加入麒麟,现任装备事业部软件工程师,主要负责实时内核研发和新技术预研等工作。
1、objtrace和ftrace的关系
我们在内核代码调试过程中,经常会遇到需要跟踪函数参数值变化的情况,objtrace是一个非常有创造性的工具,它可以将我们从笨重的调试工作中解脱出来,让我们能够很简洁,直观而且优雅的找到我们期望在代码中发现的内容。
kprobe和function tracer共建于ftrace的基础设施,objtrace将二者巧妙的结合起来,在开发者设置了kprobe的时候,objtrace打开了function tracer,并注册了回调函数。
当代码运行到kprobe关联的函数的时候,objtrace将开发者所设定的参数的地址,偏移量,大小等信息保存起来。
之后,当内核代码运行到任何一个function tracer可以跟踪的函数的时候,objtrace都会将该函数的参数依次取出来,与已经存储的,需要跟踪的参数的地址进行比对,如果发现符合条件的话,就将参数的值打印到ftrace中。
2、如何获取objtrace源代码
objtrace的源码最早由阅码场资深用户jeff xie提交给内核社区,大家可以在github上找到,大家可以下载,编译,试用,很便捷。目前objtrace应该已经到了v12,大家找到v12的分支即可。作者之前一直在v10上调试,下载代码的命令:git clone -b objtrace-v10 https://github.com/x-lugoo/linux.git
3、功能描述
我们在开发和调试代码的过程中,经常会遇到一个场景,变量在函数之间传递的过程中,我们需要追踪这个变量的值变化的过程,比如下面这个例子中的bio:
bio_add_page(struct bio *bio ...) -->__bio_try_merge_page(bio ...) --> bio_flagged(bio...) __bio_add_page(bio...)...
通常,我们会增加很多printk,然后编译安装内核,重启。如果有的函数分支忽略掉的话,还需要再增加printk,再次编译安装内核和重启,是不是很笨重?
内核中的kprobe提供了可以不用修改代码而直接查看函数参数值的方法:
cd /sys/kernel/debug/tracingecho 'p bio_add_page arg1=$arg1 arg2=$arg2' > ./kprobe_eventsecho 1 > events/kprobes/p_bio_add_page_0/enablecat your_file > /dev/nullcat trace jbd2/sda5-8-220 [000] ...1. 55531.677782: p_bio_add_page_0: (bio_add_page+0x0/0x90) arg1=0xffff8ecac98aba00 arg2=0xfffff58a0122a400 jbd2/sda5-8-220 [000] ...1. 55531.677786: p_bio_add_page_0: (bio_add_page+0x0/0x90) arg1=0xffff8ecac98abc00 arg2=0xfffff58a04141140 jbd2/sda5-8-220 [000] ...1. 55531.677790: p_bio_add_page_0: (bio_add_page+0x0/0x90) arg1=0xffff8ecac98ab100 arg2=0xfffff58a02e9f880 jbd2/sda5-8-220 [000] ...1. 55531.677794: p_bio_add_page_0: (bio_add_page+0x0/0x90) arg1=0xffff8ecac98ab300 arg2=0xfffff58a041d1b00 jbd2/sda5-8-220 [000] ...1. 55531.677798: p_bio_add_page_0: (bio_add_page+0x0/0x90) arg1=0xffff8ecac98ab600 arg2=0xfffff58a04141280...
kprobe利用了ftrace中function tracer的架构,将代码段中__fentry__(或__mcount)替换为kprobe_ftrace_handler,kprobe_ftrace_handler将函数名称和参数值打印到了ftrace的ringbuffer之中。
但还有一个问题,kprobe只能追踪变量在bio_add_page内的值,不能跟踪变量在其之后的变化。
国内社区的jeff老师近期开发了objtrace可以实现变量持续跟踪的功能,objtrace结合使用了kprobe和function tracer的功能,持续跟踪一个或多个变量,同时还可以设置偏移量,获取变量结构内部元素的值。
目前objtrace正在评审中,很快就会进入到内核代码树的主线。
4、使用方法
1)进入debugfs的目录:
cd /sys/kernel/debug/tracing
2)设置kprobe
echo 'p bio_add_page arg1=$arg1 arg2=$arg2' > ./kprobe_events查看kprobe:ls events/kprobes/ -al里面增加了p_bio_add_page_0cat events/kprobes/p_bio_add_page_0/trigger# available triggers:# traceon traceoff snapshot stacktrace enable_event disable_event enable_hist disable_hist hist objtrace可以看到除了原有的traceon traceoff snapshot stacktrace之外,增加了一个新的触发器objtrace
3)设置objtrace
echo 'objtracearg1,0x285 if comm == cat' > ./events/kprobes/p_bio_add_page_0/trigger表示在kprobe:bio_add_page需要跟踪第一个参数,也就是结构bio偏移量为0x28的变量的值。cat events/kprobes/p_bio_add_page_0/triggerobjtracearg1,0x28count=0 if comm == cat
4)触发
cat your_file > /dev/null //读取一个大文件,触发bio_add_page,也就是内核需要调用到bio_add_page函数
5)查看日志
cat trace# tracer: nop## entries-in-buffer/entries-written: 309/401 #p:4## _-----=> irqs-off/bh-disabled# / _----=> need-resched# | / _---=> hardirq/softirq# || / _--=> preempt-depth# ||| / _-=> migrate-disable# |||| / delay# task-pid cpu# ||||| timestamp function# | | | ||||| | | kworker/3:1h-79 [003] ...1. 248.076143: wbt_issue <-__rq_qos_issue object:0xffffa0c886cc8700 value:0x1000 kworker/3:1h-79 [003] ...1. 248.076143: blk_add_timer <-blk_mq_start_request object:0xffffa0c886cc8700 value:0x1000##### cpu 1 buffer started #### -0 [001] .ns2. 248.080898: bio_endio <-blk_update_request object:0xffffa0c886cc8700 value:0x0 -0 [001] .ns2. 248.080900: __rq_qos_done_bio <-bio_endio object:0xffffa0c886cc8700 value:0x0 -0 [001] .ns2. 248.080900: bio_uninit <-bio_endio object:0xffffa0c886cc8700 value:0x0 -0 [001] .ns2. 248.080901: bio_uninit <-bio_endio object:0xffffa0c886cc8700 value:0x0 -0 [001] .ns2. 248.080901: __rcu_read_lock <-bio_uninit object:0xffffa0c886cc8700 value:0x0 -0 [001] .ns2. 248.080901: __rcu_read_lock trace_init -->trace_event_init -->event_trace_enable -->register_trigger_cmds -->register_trigger_object_cmd -- > register_event_command(&trigger_object_cmd); 注册trigger_object_cmdtrigger_object_cmd包含了event_command所需要的函数,如event_object_trigger_parse就是工程是输入objtrace:add...之后,将这个字符串解析的过程。static struct event_command trigger_object_cmd = { .name = objtrace, .trigger_type = ett_trace_object, .flags = event_cmd_fl_needs_rec, .parse = event_object_trigger_parse, .reg = register_trigger, .unreg = unregister_trigger, .get_trigger_ops = objecttrace_get_trigger_ops, .set_filter = set_trigger_filter, };此外,start_kernel还有一个路径初始化objtrace的另一部分start_kernel -->early_trace_init -->tracer_alloc_buffers -->allocate_objtrace_dataallocate_objtrace_data中申请并初始化了一个结构体objtrace_data *obj_data,其中最重要的是obj_data->fops->func = trace_object_events_call,函数trace_object_events_call负责跟踪参数值并打印到ftrace的ring buffer中,可以说trace_object_events_call是objtrace中最重要的一个函数。注意,此时trace_object_events_call只是放到了obj_data->fops->func中,并没有开始使用。
5.2设置 “echo 'objtracearg1,0x285 if comm == cat' > ./events/kprobes/p_bio_add_page_0/trigger”
当开发者输入这个命令的时候,系统需要将 'objtracearg1,0x285 if comm == cat' 解析出来,并调用event_trigger_register注册到ftrace中,这样当内核走到bio_add_page这个函数的时候,就会触发kprobe。函数调用过程如下:我在event_trigger_set_filter中增加了一个warn_once,可以看到调用栈,同时在event_object_trigger_parse中增加了一个printk,看到一些变量。通常我都是使用function_grath来看代码调用的流程,但ftrace本身的函数不能使用ftrace来观测,这样会造成递归调用,很不幸不是,只能使用这种笨办法了:[ 180.024670] chensong: event_object_trigger_parse add:arg1,0x285, objtrace_cmd:add[ 180.024685] ------------[ cut here ]------------[ 180.024688] chensong: set_trigger_filter, filter_str:if comm == cat[ 180.024698] warning: cpu: 3 pid: 4332 at kernel/trace/trace_events_trigger.c:1056 set_trigger_filter+0x13d/0x170...[ 180.024844] cpu: 3 pid: 4332 comm: bash not tainted 5.18.0-rc2-objtrace-v10+ #2[ 180.024848] hardware name: lenovo yangtiant4900v-00/, bios fckt65aus 01/12/2015[ 180.024851] rip: 0010:set_trigger_filter+0x13d/0x170[ 180.024856] code: 83 c4 18 44 89 e0 5b 41 5c 41 5d 5d c3 48 89 fa 48 c7 c6 e0 15 23 84 48 c7 c7 15 e4 5a 84 c6 05 27 84 94 01 01 e8 d2 f7 9c 00 0b 48 8b 45 d0 e9 f6 fe ff ff e8 73 57 a3 00 48 8b 7b 20 41 bc[ 180.024859] rsp: 0018:ffffa28003a77d60 eflags: 00010286...[ 180.024879] cr2: 00007fbeb41fa020 cr3: 000000000a0f6003 cr4: 00000000001706e0[ 180.024882] call trace:[ 180.024884] [ 180.024888] event_trigger_set_filter+0x22/0x30[ 180.024894] event_object_trigger_parse.cold+0x313/0x345[ 180.024904] trigger_process_regex+0xc1/0x110[ 180.024908] event_trigger_write+0x70/0xd0...[ 180.024913] vfs_write+0xc0/0x2b0[ 180.024920] ksys_write+0x67/0xe0[ 180.024926] __x64_sys_write+0x1a/0x20[ 180.024931] do_syscall_64+0x3b/0x90[ 180.024937] entry_syscall_64_after_hwframe+0x44/0xae
event_object_trigger_parse首先解析’objtracearg1,0x285’,并将结果保存到变量trigger_data中,最后调用event_trigger_register,过程如下:
static int event_object_trigger_parse(struct event_command *cmd_ops,... { struct event_trigger_data *trigger_data; struct objtrace_trigger_data *obj_data; ... obj_data->field = field; --- (1) obj_data->obj_offset = offset; obj_data->obj_value_type_size = obj_value_type_size; obj_data->tr = file->tr; snprintf(obj_data->objtrace_cmd, objtrace_cmd_len, objtrace_cmd); trigger_data = event_trigger_alloc(cmd_ops, cmd, param, obj_data); ---(2) ... ret = event_trigger_set_filter(cmd_ops, file, filter, trigger_data); ---(3) if (ret reg(glob, trigger_data, file); ---(5) } int event_trigger_set_filter(struct event_command *cmd_ops, { if (param && cmd_ops->set_filter) return cmd_ops->set_filter(param, trigger_data, file); ---(6) return 0; } static struct event_command trigger_object_cmd = { --- (7) .name = objtrace, .trigger_type = ett_trace_object, .flags = event_cmd_fl_needs_rec, .parse = event_object_trigger_parse, .reg = register_trigger, .unreg = unregister_trigger, .get_trigger_ops = objecttrace_get_trigger_ops, .set_filter = set_trigger_filter, };
(1)解析的结果放到objtrace_trigger_data中。
(2)同时,将objtrace_trigger_data放置到event_trigger_data中。
(3)注册filter,也就是’if comm == cat’。
(4)注册trigger。
(5)event_trigger_register就是调用trigger_object_cmd中的.reg,也就是register_trigger。
(6)event_trigger_set_filter就是调用trigger_object_cmd中的.set_filter,也就是set_trigger_filter。
(7)trigger_object_cmd的定义。
objtrace还有一个重要的工作,就是要跟踪变量的流向,这个就超出了kprobe的功能范围,需要借助function tracer了,所以kprobe部分还有一个重要的工作,就是使能function tracer。我花了很多时间来找如何使能function tracer的,一直没有找到。但如果你了解function tracer的话,会知道在使能function tracer,必然会调用ftrace_replace_code来替换代码段,将__fentry__(或__mcount)替换为ftrace_call,那么我就在ftrace_replace_code中增加一个warn,看一下调用栈:
增加调试代码:arch/x86/kernel/ftrace.c +194 ftrace_replace_code +static int counter = 0; void ftrace_replace_code(int enable) { rec = ftrace_rec_iter_record(iter);+ warn_once(1, chensong: ip:%ps , rec->ip);+ counter++;+ if (counter%100 == 0) + printk(chensong:%s, ip:%ps, __function__, rec->ip);输入echo 'objtracearg1,0x285 if comm == cat' > ./events/kprobes/p_bio_add_page_0/trigger后,得到调用栈[ 168.674941] chensong: ip:__traceiter_initcall_level+0x0/0x40 [ 168.674954] warning: cpu: 3 pid: 3032 at arch/x86/kernel/ftrace.c:205 ftrace_replace_code+0x10b/0x1d0...[ 168.675108] cpu: 3 pid: 3032 comm: bash not tainted 5.18.0-rc2-objtrace-v10+ #6[ 168.675112] hardware name: lenovo yangtiant4900v-00/, bios fckt65aus 01/12/2015[ 168.675115] rip: 0010:ftrace_replace_code+0x10b/0x1d0[ 168.675121] code: 89 fe 89 c7 e8 f7 97 b4 00 5b 41 5c 41 5d 41 5e 41 5f 5d c3 48 8b 30 48 c7 c7 78 c5 b8 a5 c6 05 f0 6e ab 01 01 e8 4f f3 b3 00 0b eb 80 e8 bc d1 13 00 31 db 45 85 f6 49 c7 c6 30 32 49 a6 0f[ 168.675124] rsp: 0018:ffffb8a90299fc78 eflags: 00010286[ 168.675128] rax: 0000000000000000 rbx: ffffffffa6493230 rcx: 0000000000000000...[ 168.675149] call trace:[ 168.675151] [ 168.675156] ftrace_modify_all_code+0xcc/0x160[ 168.675165] arch_ftrace_update_code+0x9/0x10[ 168.675170] ftrace_run_update_code+0x1a/0x70[ 168.675176] ftrace_startup_enable+0x2e/0x50[ 168.675181] ftrace_startup+0xa8/0x120[ 168.675185] register_ftrace_function+0x25/0x70[ 168.675189] event_object_trigger_init+0x32/0x50[ 168.675194] register_trigger+0x69/0xd0[ 168.675200] event_trigger_register+0x1c/0x20[ 168.675204] event_object_trigger_parse+0x323/0x390[ 168.675211] trigger_process_regex+0xc1/0x110[ 168.675216] event_trigger_write+0x70/0xd0[ 168.675221] vfs_write+0xc0/0x2b0[ 168.675228] ksys_write+0x67/0xe0[ 168.675234] __x64_sys_write+0x1a/0x20[ 168.675240] do_syscall_64+0x3b/0x90[ 168.675246] entry_syscall_64_after_hwframe+0x44/0xae
这部分代码也是从register_trigger调用过来的,最终调用了
register_trigger -->event_object_trigger_init --> register_ftrace_function -->ftrace_modify_all_code其中,register_ftrace_function还将&tr->obj_data->fops注册到function tracer的框架中,那么函数调用function tracer时,function tracer会调用tr->obj_data->fops中的回调函数func,也就是在allocate_objtrace_data中赋值的trace_object_events_call函数,此时trace_object_events_call开始工作。
至此,function tracer被打开,以后内核中所有可以trace的函数,都将要走过function tracer这条路径,也就是每一个函数调用之后,都会运行到trace_object_events_call中。
5.3触发 “cat /home/kylin/workspace/opensource/linux-objtrace-jeff/kernel/sched/core.c > /dev/null”
本节是objtrace中最重要的一个环节,当kprobe和objtrace都设置好了,一旦内核调用到bio_add_page,就要开始跟踪bio这个变量了,objtrace是如何在原有的kprobe的基础上实现这个功能的呢?
我在阅读代码的时候,将这个过程分为两部分,一个是kprobe,另一个是function tracer,我们先来看看当kprobe被触发的时候,objtrace做了什么工作:
同样的,我还是使用了printk和warn_once来进行调试,我首先在函数set_trace_object中增加了一个warn_once,得到的调用栈如下:[ 248.069971] chensong:set_trace_object, call stack of set trace object[ 248.069980] warning: cpu: 3 pid: 4343 at kernel/trace/trace_object.c:162 trace_object_trigger+0x184/0x1a0....[ 248.070450] hardware name: lenovo yangtiant4900v-00/, bios fckt65aus 01/12/2015[ 248.070455] rip: 0010:trace_object_trigger+0x184/0x1a0[ 248.070462] code: 7a ae 48 c7 c6 2d 69 3d ad e8 28 82 fe ff eb bc 48 c7 c6 60 05 43 ae 48 c7 c7 80 ce 7a ae c6 05 2c 11 95 01 01 e8 db 84 9d 00 0b eb 9e 48 85 db 0f 84 16 ff ff ff 48 c7 c0 80 96 bc ae e9 d1[ 248.070467] rsp: 0018:ffffb122007d36a8 eflags: 00010086....[ 248.070515] call trace:[ 248.070520] [ 248.070533] trace_object_count_trigger+0x1e/0x30[ 248.070541] event_triggers_call+0x5d/0xe0[ 248.070556] trace_event_buffer_commit+0x1a2/0x260[ 248.070573] kprobe_trace_func+0x1ad/0x2a0[ 248.070602] kprobe_dispatcher+0x42/0x70[ 248.070610] ? bio_add_page+0x1/0x90[ 248.070622] kprobe_ftrace_handler+0x168/0x1f0[ 248.070629] ? bio_add_page+0x5/0x90[ 248.070636] ? __bio_try_merge_page+0x140/0x140[ 248.070648] arch_ftrace_ops_list_func+0xe5/0x160[ 248.070655] ? submit_bh_wbc+0xca/0x140[ 248.070677] ftrace_regs_call+0x5/0x52[ 248.070689] rip: 0010:bio_add_page+0x1/0x90....[ 248.070781] ? bio_add_page+0x5/0x90[ 248.070789] ? submit_bh_wbc+0xca/0x140
我们可以看到,当bio_add_page被触发之后,进入和kprobe的调用路径:
bio_add_page -->ftrace_regs_call -->arch_ftrace_ops_list_func-->kprobe_ftrace_handler -->kprobe_dispatcher -->kprobe_trace_func -->event_triggers_call --> trace_object_count_trigger --> set_trace_object
其中trace_object_count_trigger也是trigger_object_cmd中定义的回调函数,在trigger条件符合被触发的时候被调用,我在set_trace_object中还增加了一个printk,打印obj的地址,也就是需要跟踪的变量的地址。
static void set_trace_object(void *obj, int obj_offset, int obj_value_type_size, struct trace_array *tr){ unsigned long flags; struct object_instance *obj_ins; struct objtrace_data *obj_data;... obj_ins = &obj_data->traced_obj[atomic_read(&obj_data->num_traced_obj)]; obj_ins->obj = obj; --- (1) printk(chensong:%s, obj:0x%lx, __function__, obj); obj_ins->obj_value_type_size = obj_value_type_size; obj_ins->obj_offset = obj_offset; obj_ins->tr = tr; /* make sure the num_traced_obj update always appears after traced_obj update */ smp_wmb(); atomic_inc(&obj_data->num_traced_obj); //warn_once(1, chensong:%s, call stack of set trace object, __function__); out: raw_spin_unlock_irqrestore(&obj_data->obj_data_lock, flags); }
(1) 这个时候内核已经走到了bio_add_page,所以已经知道了bio的地址了,所以需要将bio的地址保存到object_instance的obj中,object_instance中其余的元素保存偏移量,变量大小等信息。
接下来是function tracer的部分,在上一部分讲过,开发者运行命令“echo 'objtracearg1,0x285 if comm == cat' > ./events/kprobes/p_bio_add_page_0/trigger”之后,function tracer被打开,以后内核中所有可以trace的函数,都将要走过function tracer这条路径,每一个函数调用之后,都会运行到trace_object_events_call中,trace_object_events_call的代码如下:
static voidtrace_object_events_call(unsigned long ip, unsigned long parent_ip, struct ftrace_ops *op, struct ftrace_regs *fregs){ struct pt_regs *pt_regs = ftrace_get_regs(fregs); int cpu, n; for (n = 0; n < max_args_num; n++) { obj = regs_get_kernel_argument(pt_regs, n); --- (1) if (object_exist((void *)obj, tr)) { --- (2) ... if (get_object_value(&val, (void *)(obj + obj_offset), val_type_size)) --- (3) continue; submit_trace_object(ip, parent_ip, obj, val, tr); ---(4) }...}static inline unsigned long regs_get_register(struct pt_regs *regs, unsigned int offset){ return *(unsigned long *)((unsigned long)regs + offset); --- (5)}static bool object_exist(void *obj, struct trace_array *tr){ ... for (i = 0; i traced_obj[i].obj == obj) --- (6) return true; } return false;}
(1)function tracer使能后,每个可以trace的函数都会调用到trace_object_events_call中,调用栈如下:
any kernel tracable function -->ftrace_regs_call --> arch_ftrace_ops_list_func --> __ftrace_ops_list_func --> op->func(ip, parent_ip, op, fregs); --> trace_object_events_call --> get_object_value
trace_object_events_call首先调用regs_get_kernel_argument,依次获得函数的参数,本例中为bio_add_page的参数。
(2)调用object_exist来检查是不是需要跟踪的变量,这个变量的地址在开发者输入”cat file > /dev/null”之后, 内核代码走到了bio_add_page,bio就会在set_trace_object中保存到了object_instance的obj中,所以在这个时候,后续函数如果使用了bio,object_exist返回为true。如果内核代码没有走到过bio_add_page或者该函数的参数中没有bio,object_exist会返回false。
(3)获取变量值,保存到val中。
(4)调用submit_trace_object将value打印到ftrace的ringbuffer中。
(5)regs_get_kernel_argument最终会调用到regs_get_register中,返回指定参数的地址。
(6)object_exist通过比较地址来判断,当前函数的参数中是不是包含需要跟踪的变量。
好了,到此为止,objtrace的大部分工作都结束了,找到了变量,获得了它的值,并打印到ringbuffer中,很巧妙,避免了大量的printk和warn这样笨重的方法。
但objtrace打开了所有函数的function tracer,每个函数都要去调用trace_object_events_call,还有调用object_exist进行变量的比对,会增加一定的系统开销,所以objtrace最好不用和性能调试的trace一起工作。
5.4打印 “cat trace”
从debugfs的trace文件中打印到中断,显示给开发者,调用栈如下:
[ 1617.394055] chensong:trace_object_print[ 1617.394073] warning: cpu: 2 pid: 4761 at kernel/trace/trace_output.c:1567 trace_object_print+0x73/0x90[ 1617.394733] rip: 0010:trace_object_print+0x73/0x90[ 1617.394749] code: c6 83 b5 fa b8 e8 9d 0d 00 00 4c 89 e7 be 0a 00 00 00 e8 d0 0a 00 00 48 c7 c6 30 f5 c2 b8 48 c7 c7 90 b5 fa b8 e8 ec a6 9e 00 0b 4c 89 e7 e8 23 37 ff ff 5b 41 5c 5d c3 0f 0b eb 97 66 2e 0f[ 1617.394841] call trace:[ 1617.394850] [ 1617.394864] print_trace_line+0x25e/0x570[ 1617.394878] ? ring_buffer_iter_advance+0x32/0x40[ 1617.394902] s_show+0x4c/0x160[ 1617.394919] seq_read_iter+0x2a3/0x450[ 1617.394947] seq_read+0xee/0x120[ 1617.394983] vfs_read+0xa7/0x1a0[ 1617.395006] ksys_read+0x67/0xe0[ 1617.395029] __x64_sys_read+0x1a/0x20[ 1617.395042] do_syscall_64+0x3b/0x90[ 1617.395056] entry_syscall_64_after_hwframe+0x44/0xae
原文标题:ftrace源码分析系列之objtrace
文章出处:【微信公众号:linux阅码场】欢迎添加关注!文章转载请注明出处。
基于物联网的资产跟踪解决方案
小米6什么时候上市?小米6正脸首次曝光:超窄边框 ID无边框再见!
用于包络跟踪电源的多相三电平降压转换器
用NaOH和KOH溶液蚀刻硅晶片的比较研究
arm是单片机吗_arm与单片机的区别
objtrace使用方法与代码分析
单片机与ARM谁更实用
基于英特尔FPGA的全新解决方案
广东省补齐芯片产业链短板,研发自主核心技术迫在眉睫
电流传感器选型要点
SCR后处理系统故障确认与注意事项
PTC和PT100保护电机的方法
自制头部追踪器教程
集成功率放大器电路图
登临科技华东总部项目落户进一步推动区域集成电路产业“指数级”增长
芯片“卡脖子”,中美科技战雷声大,雨点小?
PCB厂商金禄电子创业板IPO已获受理
基于STM32的酒精浓度检测报警proteus仿真设计
2021年网络安全行业将迎来什么挑战?
捆扎机远程监控运维数字化解决方案