
前言数年前有一个项目当时使用了gcov抓取了用户态代码的覆盖率。我认为是一个很好的工具和方法。如果有代码不能运行到要么就是测试用例不充分要么就是代码是冗余无效的代码。做了几年嵌入式了用户态可以借助linux系统的机制去抓对于单片机这种没有linux环境的项目如何抓取覆盖率呢。这个疑问在我脑中一直滞留。这不最近职场Loser跑路避难结束在日本成功就职工作是写点STM32。最近也不加班了工资降了不少但WLB基本实现了。所以这个念头…可以实现以下。我也就来尝试一下单片机能不能抓取覆盖率数据。其实是可以的github上有一个嵌入式gcov的项目我看到之后就在想理论上可以为什么不去试一下呢于是我就自己动手在qemu的环境下尝试了一下。环境Linux虚拟机一台。qemu其他什么交叉gccpythonmake你能点进来看我想应该都知道吧。我对IDE环境不太熟就在linux下手搓了理论上IDE环境也可以代码#includestdint.hexternuint32_t_estack;externuint32_t_sidata;externuint32_t_sdata;externuint32_t_edata;externuint32_t_sbss;externuint32_t_ebss;intmain(void);voidReset_Handler(void);voidDefault_Handler(void);voidNMI_Handler(void)__attribute__((weak,alias(Default_Handler)));voidHardFault_Handler(void)__attribute__((weak,alias(Default_Handler)));voidMemManage_Handler(void)__attribute__((weak,alias(Default_Handler)));voidBusFault_Handler(void)__attribute__((weak,alias(Default_Handler)));voidUsageFault_Handler(void)__attribute__((weak,alias(Default_Handler)));voidSVC_Handler(void)__attribute__((weak,alias(Default_Handler)));voidDebugMon_Handler(void)__attribute__((weak,alias(Default_Handler)));voidPendSV_Handler(void)__attribute__((weak,alias(Default_Handler)));voidSysTick_Handler(void)__attribute__((weak,alias(Default_Handler)));voidTIM2_IRQHandler(void)__attribute__((weak,alias(Default_Handler)));__attribute__((section(.isr_vector)))constvoid*vector_table[]{_estack,Reset_Handler,NMI_Handler,HardFault_Handler,MemManage_Handler,BusFault_Handler,UsageFault_Handler,0,0,0,0,SVC_Handler,DebugMon_Handler,0,PendSV_Handler,SysTick_Handler,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,TIM2_IRQHandler,};voidReset_Handler(void){uint32_t*src_sidata;uint32_t*dst_sdata;while(dst_edata){*dst*src;}for(dst_sbss;dst_ebss;dst){*dst0;}(void)main();while(1){}}voidDefault_Handler(void){while(1){}}#includestm32f100.h#includestdint.h#includegcov-io-uart.h#defineSYSTEM_CORE_CLOCK_HZ8000000UL#ifndefTIMER_IRQ_PERIOD_MS#defineTIMER_IRQ_PERIOD_MS100U#endifstaticvolatileuint32_tg_elapsed_ms;staticvolatileuint32_tg_timer_irq_count;staticvoidirq_enable(void){__asmvolatile(cpsie i);}staticvoiddelay_ms(uint32_tms){constuint32_tstartg_elapsed_ms;while((uint32_t)(g_elapsed_ms-start)ms){}}staticvoiduart_putc(usart_t*uart,charch){if(ch10){uart_putc(uart,13);}while((uart-SRUSART_SR_TXE)0U){}uart-DR(uint32_t)ch;}staticvoiduart_puts(usart_t*uart,constchar*text){while(*text!0){uart_putc(uart,*text);}}staticvoiduart_put_u32(usart_t*uart,uint32_tvalue){charbuf[10];uint32_tindex0;if(value0U){uart_putc(uart,48);return;}while(value0Uindexsizeof(buf)){buf[index](char)(48U(value%10U));value/10U;}while(index0U){uart_putc(uart,buf[--index]);}}staticvoiduart1_init(void){RCC-APB2ENR|RCC_APB2ENR_AFIOEN|RCC_APB2ENR_IOPAEN|RCC_APB2ENR_USART1EN;GPIOA-CRH~(0xFUL4);GPIOA-CRH|(0xBUL4);/* PA9 USART1_TX. */USART1-CR10;USART1-BRR0x0045;/* 8 MHz / 115200 baud. */USART1-CR1USART_CR1_TE|USART_CR1_UE;}staticvoiduart2_init(void){RCC-APB2ENR|RCC_APB2ENR_AFIOEN|RCC_APB2ENR_IOPAEN;RCC-APB1ENR|RCC_APB1ENR_USART2EN;GPIOA-CRL~((0xFUL8)|(0xFUL12));GPIOA-CRL|(0xBUL8)|(0x4UL12);/* PA2 TX, PA3 RX floating input. */USART2-CR10;USART2-BRR0x0045;/* 8 MHz / 115200 baud. */USART2-CR1USART_CR1_RE|USART_CR1_TE|USART_CR1_UE;}staticvoidtimer_irq_init(void){constuint32_treload((SYSTEM_CORE_CLOCK_HZ/1000UL)*TIMER_IRQ_PERIOD_MS)-1UL;SYSTICK-CTRL0;SYSTICK-LOADreload;SYSTICK-VAL0;SYSTICK-CTRLSYSTICK_CTRL_CLKSOURCE|SYSTICK_CTRL_TICKINT|SYSTICK_CTRL_ENABLE;}voidSysTick_Handler(void){g_elapsed_msTIMER_IRQ_PERIOD_MS;g_timer_irq_count;}intmain(void){uint32_tseconds0;uart1_init();uart2_init();timer_irq_init();irq_enable();uart_puts(USART1,stm32vldiscovery qemu demo\n);while(seconds10){delay_ms(2000U);seconds;uart_put_u32(USART1,seconds);uart_puts(USART1,\n);if(0){uart_puts(USART1,TEST---\n);}}uart_puts(USART1,TEST---\n);gcov_data_dump_to_uart();}以上这些都是AI生成的哈大致就是这么一个思路。我们直接说重要的改动。1.链接脚本需要新增一个区域这个区域要用来放gcov的数据.gcov_info:{.ALIGN(4);__gcov_info_start.;KEEP(*(.gcov_info))KEEP(*(.gcov_info.*))__gcov_info_end.;.ALIGN(4);}FLASH2.编译参数编译和链接要新增参数不然编译不会自动为代码插装COVERAGE_CFLAGS :-fprofile-arcs -ftest-coverage -fprofile-info-section COVERAGE_LDFLAGS :--coverage3.新增数据导出我是直接把数据从串口打印出来的因此需要专门写一个dump函数借助gcov本身的函数去封装自己的dump机制#includestddef.h#includestdint.h#includestm32f100.h#includegcov-io-uart.hexternconststructgcov_info*const__gcov_info_start[];externconststructgcov_info*const__gcov_info_end[];staticvoiduart2_putc(charch){while((USART2-SRUSART_SR_TXE)0U){}USART2-DR(uint32_t)ch;}void__gcov_filename_to_gcfn(constchar*filename,void(*dump_fn)(constvoid*data,unsignedlength,void*arg),void*arg);void__gcov_info_to_gcda(conststructgcov_info*info,void(*filename_fn)(constchar*filename,void*arg),void(*dump_fn)(constvoid*data,unsignedlength,void*arg),void*(*allocate_fn)(unsignedlength,void*arg),void*arg);staticvoiddump_hex_byte(uint8_tvalue){staticconstcharhex[]0123456789abcdef;uart2_putc(hex[(value4)0x0FU]);uart2_putc(hex[value0x0FU]);}staticvoidcoverage_dump_bytes(constvoid*data,unsignedlength,void*arg){constuint8_t*bytes(constuint8_t*)data;unsigned*column(unsigned*)arg;for(unsignedi0;ilength;i){dump_hex_byte(bytes[i]);*column2U;if(*column64U){uart2_putc(\n);*column0;}}}staticvoidcoverage_dump_filename(constchar*filename,void*arg){__gcov_filename_to_gcfn(filename,coverage_dump_bytes,arg);}staticvoid*coverage_allocate(unsignedlength,void*arg){staticuint8_tbuffer[512];(void)arg;if(lengthsizeof(buffer)){returnNULL;}returnbuffer;}voidgcov_data_dump_to_uart(void){unsignedcolumn0;for(conststructgcov_info*const*info__gcov_info_start;info!__gcov_info_end;info){__gcov_info_to_gcda(*info,coverage_dump_filename,coverage_dump_bytes,coverage_allocate,column);}}此外还有点编译问题会提示链接找不到因为gcov默认是需要文件系统的因此需要提供一些文件系统的假接口。不会链接会报错。#includestddef.h#includestdint.hexternuint32_t_end;externuint32_t_estack;void_exit(intstatus){(void)status;while(1){}}int_close(intfile){(void)file;return-1;}int_fstat(intfile,void*st){(void)file;(void)st;return0;}int_getpid(void){return1;}int_isatty(intfile){(void)file;return1;}int_kill(intpid,intsig){(void)pid;(void)sig;return-1;}int_lseek(intfile,intptr,intdir){(void)file;(void)ptr;(void)dir;return0;}int_read(intfile,char*ptr,intlen){(void)file;(void)ptr;(void)len;return0;}int_write(intfile,constchar*ptr,intlen){(void)file;(void)ptr;returnlen;}void*_sbrk(ptrdiff_tincr){staticuint8_t*heap_end;uint8_t*prev_heap_end;if(heap_endNULL){heap_end(uint8_t*)_end;}prev_heap_endheap_end;if((heap_endincr)(uint8_t*)_estack){return(void*)-1;}heap_endincr;returnprev_heap_end;}void_init(void){}void_fini(void){}3.其他脚本qemu运行脚本#!/usr/bin/env shset-eu ELF${1:-build/gcov_all/firmware.elf}exec qemu-system-arm \-M stm32vldiscovery \-cpu cortex-m3 \-kernel$ELF\-nographic \-serial mon:stdio \-serial ptypython获取串口数据脚本#!/usr/bin/env python3importosimportsysimporttimeimportbinascii device_name/dev/pts/5deadlinetime.time()10fdos.open(device_name,os.O_RDONLY|os.O_NOCTTY|os.O_NONBLOCK)hex_bufferbdefwait_for_start():globalfdglobaldeadlineglobalhex_buffertry:dataos.read(fd,1024)exceptBlockingIOError:returndeadlinetime.time()5iflen(data):datadata.replace(b\n,b)datadata.replace(b\r,b)datadata.replace(b ,b)hex_bufferdataif__name____main__:whiletime.time()deadline:wait_for_start()print(over)os.close(fd)print(hex_buffer,len(hex_buffer))data_writebinascii.unhexlify(hex_buffer)withopen(stream.bin,ab)asf:f.write(data_write)运行结果如下,目录下的bin就是全部的覆盖率数据。4.后续操作# 提交覆盖率数据arm-none-eabi-gcov-toolmerge-streamstream.bin# 获取覆盖率lcov--capture--directory ${dir}--output-file coverage.info--gcov-tool arm-none-eabi-gcov# 生成htmlgenhtml coverage.info--output-directory html5.结果以上 OVER总结所以理论上单片机也可以测试覆盖率的。唯一的问题在于内存是否足够。全局插装编译代码体积会暴增。我这有一块stm32的f303目前代码编译完70多K我全局插装编完提示内存缺30K当然我这个项目库比较多如果我只插装业务还是够的但可执行文件好像也变成了120多K所以总的来说内存是限制其他的都还好。