打印函数的底层实现逻辑

《操作系统——真相还原》第六章笔记

Q&A

Q1:函数调用中的参数存放在哪里?
A1:有两种方案,一种是保存在寄存器中,一种是保存在内存中。但因为要考虑到多进程的参数覆盖问题,所以索性将函数的参数放在内存当中(32位机器,64位机器前六个参数用寄存器传递)。
又因为每个进程都有自己的栈,所以将函数的参数存放在各自进程的栈中。

Q2:计算机如何知道具体的参数?
A2:A1说了函数的参数存放在各自的栈中,但函数可能又不只一个参数,计算机如果知道具体的参数在栈中的位置呢?这就涉及到了函数调用约定
函数调用约定规定了函数参数的压栈顺序,和主调函数被调函数谁来回收栈空间。

Q3:打印函数的实现逻辑?
1、语言:不管是printf,还是cout等输出函数,其本质都是写显存。因为没有高级语言支持直接写显存,所以只能用汇编来实现, 并将函数名称用global全局符号标注,使链接器能够发现。

global put_str
put str:

2、逻辑(以显示单个字符为例):
(1)备份寄存器现场 pushad ;会将所有32位寄存器压栈
(2)获取光标坐标值 (涉及到读取显卡的端口,这个不做重点,blog中就不再写了,十分折腾人)
(3)获取待打印的字符mov ecx [esp+36];我们是32位linux系统,参数传递通过栈来实现,pushad会将8个32位寄存器压栈,之上是返回地址,因此需要+36取得参数
(4)对字符进行判断,控制字符还是可见字符?
控制字符包括回车,后退等,除此之外都是可见字符。
对每个控制字符采用一种处理逻辑,可见字符采用统一的处理逻辑。
(5)判断是否需要滚屏?
如果待显示的字符超过了屏幕当先的显示范围,则需要滚屏
(6)更新光标,指向下一个字符

多个字符的显示逻辑和单个字符一样。
话说代码层面需要和显卡端口打很多的交道,调试特别烦人,实话实说,我并没有自己独立实现,而是照搬了《操作系统真相还原》一书中的代码,以后有时间再做尝试。

函数调用约定:

函数调用约定按照类别可分为调用者清理和被调用者清理两大类:
在这里插入图片描述
我们用的是cdcel调用约定其最大的亮点是允许函数中参数的数量不固定,cdcel调用约定源自C语言,又称为C调用约定,具体为:
(1)调用者将参数从右向左入栈。
(2)调用者负责清理栈空间。
系统调用输入参数的传递方式:
但输入参数小于5个的时候,用寄存器传递参数,顺序为:
ebx,ecx,edx,esi,edi

C和汇编混合编程

C的汇编的混合编程方式:
(1)内联汇编(过后补充)
(2)单独的C和汇编链接成可执行程序

1、内联汇编:


2、c和汇编链接成可执行文件:
举个C调用汇编的例子,汇编使用系统调用打印hello world!:
test.c:

extern void asm_print(char* ,int ); //声明外部符号
int main()
{
  char*s = "Hello world!\n";
  int len = 0;
  while(s[len++]);
  asm_print(s,len);
  return 0;
}

sys.S

section .text
global asm_print  //声明为全局符号,使链接器能够链接
asm_print:
push ebp;
mov ebp, esp
mov eax, 4  ;write中断调用号
mov ebx, 1  ;文件描述符,标准输出指向屏幕
mov ecx, [ebp+8] ; No.1 parameter
mov edx, [ebp+12] ; No.2 parameter
int 0x80;
pop ebp;
ret

编译连接命令:
(1)将.c文件和.S文件汇编为.o文件:
.c文件: gcc -m32 -c -o test.o test.c
.S文件: nasm -f elf -o sys.o sys.S
!注意 因为汇编是在32位调用约定下,所以必须加上-m32使.c文件在32位环境下编译。
(2)将两个.o文件进行链接:
ld -m elf_i386 -e main -o test sys.o test.o
-e main是告诉链接器将main符号作为程序的入口地址
执行结果:在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1、CAN收发队列 使用内存FIFO缓冲CAN帧,适合大数据量通信;并使用内部软中断处理CAN数据,相当于事件响应,综合应该比查询方式节省不少时间,也应该比OS调度省点时间。Can.C:底层处理,Communi.C:与应用层高相关。 应用层处理流程用函数指针表的方式调用减少代码量及阅读整齐;实现CAN各种错误记录机制。已初步测试,该机制可用。 CAN处理流程: 接收:CAN1_RX0_IRQHandler_Name (void), CAN1_RX1_IRQHandler(void) 接收中断,CAN_QueueWriteQuick()将当前的有效报文压入内存FIFO,压入的数据为整个CAN邮箱数据,所以后续的处理函数可以分辨出完整的数据。 void CAN1_RX0_IRQHandler_Name (void) // CAN1_RX0_IRQHandler_Name { /* FIFO从空状态开始,在接收到第一个有效的报文后,FIFO状态变为挂号_1(pending_1), 硬件相应地把CAN_RFR寄存器的FMP[1:0]设置为’01’(二进制01b)。 软件可以读取FIFO输出邮箱来读出邮箱中的报文,然后通过对CAN_RFR寄存器的RFOM位 设置’1’来释放邮箱,这样FIFO又变为空状态了。如果在释放邮箱的同时, 又收到了一个有效的报文,那么FIFO仍然保留在挂号_1状态,软件可以读取FIFO 输出邮箱来读出新收到的报文。 如果应用程序不释放邮箱,在接收到下一个有效的报文后,FIFO状态变为 挂号_2(pending_2),硬件相应地把FMP[1:0]设置为’10’(二进制10b)。 重复上面的过程,第三个有效的报文把FIFO变为挂号_3状态(FMP[1:0]=11b)。 此时,软件必须对RFOM位设置1来释放邮箱,以便FIFO可以有空间来存放下一个有效的 报文;否则,下一个有效的报文到来时就会导致一个报文的丢失。 */ while (CAN1->RF0R & CAN_RF0R_FMP0) // message pending ? { CAN_QueueWriteQuick(&CanRxQueue;, (T_CanFrame *)&CAN1;->sFIFOMailBox[CAN_FIFO0]); CAN1->RF0R |= CAN_RF0R_RFOM0; // Release FIFO 0 output mailbox #if CAN1_SWI_HANDLE_EN > 0 /* Add by Xsky 2011-06-18 15:48 */ EXTI->SWIER |= CAN1_SWI_EXTI_LINE; /* Add by Xsky 2011-06-18 15:47 */ #endif } } void CAN1_RX1_IRQHandler (void) { while (CAN1->RF1R & CAN_RF1R_FMP1) // message pending ? { CAN_QueueWriteQuick(&CanRxQueue;, (T_CanFrame *)&CAN1;->sFIFOMailBox[CAN_FIFO1]); CAN1->RF1R |= CAN_RF1R_RFOM1; // Release FIFO 1 output mailbox #if CAN1_SWI_HANDLE_EN > 0 /* Add by Xsky 2011-06-18 15:48 */ EXTI->SWIER |= CAN1_SWI_EXTI_LINE; /* Add by Xsky 2011-06-18 15:47 */ #endif } } 接收中断响应后,触发STM32的内部软中断(EXTI->SWIER |= CAN1_SWI_EXTI_LINE;), 实现当CAN硬件中断响应完成后,触发更低优先级的中断去处理内存中的CAN数据队列,如果处理时再发生新的CAN硬件接收中断,则会先响应硬件中断,以减少或不丢失CAN FIFO邮箱数据。处理函数在Communi.C中实现。 发送,CAN_SendFrame(): 发送时如果邮箱有空则直接将数据压入邮箱,否则将数据压入内存发送队列。等待上一次数据发送完成时,在发送中断中提取FIFO发送队列中的下一帧数据并发出。 Communi.C的功能为与应用层相关度较高的函数,如发送应用层帧,记录错误。 CAN1_SWI_Handler (void) 实现CAN接收中断触发的内部软件中断,处理内存FIFO接收的CAN数据(实际编译函数名为:EXTI4_IRQHandler())。 处理过程优化:通过定义顺序的code码,查表调用处理函数列表指针可实现比较整齐并有效率的代码机制。 CAN1_SCE_IRQHandler()实现进行错误记录(g_History.SysErrors.xxx以便于统计CAN错误)及相应处理。 个人认为这种处理方式,近似于OS的多任务,同时减少调度开销,是在可重用性与效率之间的平衡用法。当然这种处理方式,也适合于做为uCOS中的底层驱动文件,已留有CAN_QUE_OS_ENTER_CRITICAL()的宏定义,更改为相应的OS开关中断函数基本即可用于uCOS。 附 CAN总线利用率及最坏时间估算.xls, 根据应用层估计的数据发送频度最大值,自动估计CAN总线上导致的最大延时是否满足应用需求。 2、UART模板 UART DMA/中断处理方式 文件模板,可仅修改头部定义实现完全配置某指定的UART端口,以实现执行效率与代码重用的折中,UART.C,UARTx.H,UARTn.C。 UARTx.H为公共代码文件,#include被包含在UART1.C,、UART2.C、……中(用UARTn.C指代)实现所有的接收、发送的中断处理函数,在UARTn.C中宏定义各中断向量函数名以及各种硬件相关参数,定义接收发送的内存缓冲区长度等。代码实现DMA及中断响处理两种方式,通过宏定义选择编译不同代码,接收使用定时器实现字符超时指示功能,DMA接收时多使用了一级DMA接收专用内存缓冲RxDMABuf,因为DMA只能按地址连续写内存。 接收发送均使用内存缓冲区,以尽量避免中断响应时间导致的接收数据丢失问题,以及避免查询等待方式的较低效率。 发送函数:UART1_SendBytes(),UART2_SendBytes(),... 检查接收缓冲区字节数:UART1_RcvdSize() 读取指定的字节数:UART1_ReadBytes() 上层使用方法:循环检测UART1_RcvdSize()是否大于0,大于则进行读取等下一步处理,也可再定义高一级的应用层帧缓冲,以实现应用层的完整帧处理,或者增加一个对接收FIFO的预读功能,即读取时对接收FIFO中的帧进行识别,如果不是完整的应用层帧则再等待数ms或者再等待数次,等待失败则超时丢弃本帧,寻找下一帧。当然也可以在中断中增加事件机制,类似CAN中断触发低优先级软件中断,多个串口可在同一个软件中断服务中处理。 调试输出DbgPrintf函数,Function.C。 已使用大量连续数据测试该机制收发均可用,UART1~5均可用。使用本方式的考虑是在执行效率与代码重用间的平衡,部分代码使用了ST的库,如初始化时不时间使用不高时,而中断处理则基本是直接操作寄存器。并且均考虑了做为uCOS的接口,直接替换UARTx_ENTER_CRITICALx()、UARTx_EXIT_CRITICALx()函数应该可以基本实现做为uCOS的底层驱动。 注:包含UARTx.H的方式,各个UARTn.C文件重用其中代码, 某些情况下编译器可能会编译出错误的问题,具体原因还不明。但方法确实是可行的,已测试STM32F103VCT6 UART1~5均可。 3、用逻辑分析仪测定过的延时函数:Delay.C,内核72MHz,具体延时时间已注释标注. 如: void Delay_Nms(unsigned long N) { long count;//=14200; while(N) { count = 7998; /* 逻辑分析仪测试, 包含引脚取反时间 14200, 80ms: 142.026ms, 10000, 80ms: 100.025ms 8000, 80ms: 80.25ms 7975, 80ms: 79.775ms 7990, 100ms: 99.907ms 7998, 150ms: 150.01ms 7997, 150ms: 149.992ms */ while(count--); /* while(count--); 0x08001236 000A MOVS r2,r1 0x08001238 F1A10301 SUB r3,r1,#0x01 0x0800123C 4619 MOV r1,r3 0x0800123E D1FA BNE 0x08001236 */ N--; } } 4、输入检测 中断定时进行输入扫描,定义有效无效电平消抖时间,且定义按下弹起事件响应函数指针,应该会比循环扫描节省很多时间IOInput.C。已测试,机制完好。 // 常量表定义 typedef void t_IOIN_EVENT_DO(T_IOEvent); typedef uint8 t_IOIN_Counter; typedef struct t_IOIN_INFO_ { uint8 ID; // 输入信号索引 T_ValidVoltage ValidVoltage; // 有效电平,高电平或者低电平有效 __IO uint32_t *IDR; // 输入引脚指针 uint32_t PinBitMsk; // 输入引脚位掩码 t_IOIN_Counter CntValid; // 有效状态计数, 注意如果改变ValidVoltage, 此相应需要修改此参数,为确认该电平输入为有效电平的计数值,相当于滤波参数 t_IOIN_Counter CntInvalid; // 无效状态计数, 注意如果改变ValidVoltage, 此相应需要修改此参数,为确认该电平输入为无效电平的计数值,相当于滤波参数 t_IOIN_EVENT_DO *EventHandler; // 执行函数指针 }t_IOIN_INFO; static const t_IOIN_INFO IOIN_InfoTbl[IOIN_NUMS] = { // index, ValidVol, GPIO->IDR, IO_MASK, CNT, CNT~, EventHandler {IOIN_Key1, VOL_Low, &GPIOB;->IDR, BIT( 0), 4, 4, NULL }, {IOIN_Key2, VOL_Low, &GPIOB;->IDR, BIT( 1), 4, 4, NULL }, {IOIN_Key3, VOL_Low, &GPIOB;->IDR, BIT( 6), 4, 4, NULL }, {IOIN_Key4, VOL_Low, &GPIOB;->IDR, BIT( 7), 4, 4, KeyESC_Event }, {IOIN_CenterSensor, VOL_High, &GPIOC;->IDR, BIT( 7),10,10, ColorSensorEvent }, // 使其常态电平5ms推迟于颜色检测线(以检测), 有效电平2ms提早于颜色线 {IOIN_VacuumHousing,VOL_Low, &GPIOE;->IDR, BIT(12), 4,10, NULL }, {IOIN_BallCounter, VOL_Low, &GPIOE;->IDR, BIT(15), 4, 4, NULL }, {IOIN_Spin, VOL_Low, &GPIOE;->IDR, BIT(14), 3, 3, NULL }, {IOIN_ColorRed, VOL_Low, &GPIOD;->IDR, BIT( 8),16, 6, NULL }, // 使常态电平3ms提前于庶断传感器, 有效电平8ms推迟于遮断传感器 {IOIN_ColorGreen, VOL_Low, &GPIOD;->IDR, BIT( 9),16, 6, NULL }, {IOIN_ColorBlue, VOL_Low, &GPIOD;->IDR, BIT(10),16, 6, NULL } }; 使用时主要修改以上这张表的指向及消科参数,并且对中断处理函数中的顺序或者扫描的最小间隔进行区分即可。 5、颜色传感器驱动 颜色传感器TCS3200D驱动ColorSensor.C。已测试,机制完好。测量频率范围 20Hz~120KHz, S0:S1:HL。 6、铁电驱动 SPI方式读写铁电,实现片写片读函数Spi_FRAM.C;参数及历史记录读写检测函数GameParam.C。 7、其它一些可参考的模板、文件、函数、或者小的方式方法等。 by Xsky 原创STM32项目处理方法(其中个别有文件为直接用的,已注明,如周立功的)。 仅供参考交流,QQ:1821587421 其它可交流方案: GPS车辆监控系统:终端原理图PCB源码整套(稳定成熟可接多个外设);平台整套源码。 LED屏:公交,出租等 原理图PCB;PC端软件等;PDA控制LED屏程序源码。 DVR:小型SD卡录像方案,可485拍照。 公交报站器,原理图PCB;PC端软件。51版,STM32版。 汽车电动台阶驱动板原理图PCB。 PDA扫描轮:条形码扫描,GPRS上传;终端原理图PCB源码整套,服务端源码 手持公交售票终端源码,可打印小票。终端价位特低。 稳定使用的固态继电器原理及PCB(光耦隔离控制双向可控硅)。 直流电机驱动板。
API之网络函数1. API之网络函数 WNetAddConnection 创建同一个网络资源的永久性连接 WNetAddConnection2 创建同一个网络资源的连接 WNetAddConnection3 创建同一个网络资源的连接 WNetCancelConnection 结束一个网络连接 WNetCancelConnection2 结束一个网络连接 WNetCloseEnum 结束一次枚举操作 WNetConnectionDialog 启动一个标准对话框,以便建立同网络资源的连接 WNetDisconnectDialog 启动一个标准对话框,以便断开同网络资源的连接 WNetEnumResource 枚举网络资源 WNetGetConnection 获取本地或已连接的一个资源的网络名称 WNetGetLastError 获取网络错误的扩展错误信息 WNetGetUniversalName 获取网络中一个文件的远程名称以及/或者UNC(统一命名规范)名称 WNetGetUser 获取一个网络资源用以连接的名字 WNetOpenEnum 启动对网络资源进行枚举的过程 2. API之消息函数 BroadcastSystemMessage 将一条系统消息广播给系统中所有的顶级窗口 GetMessagePos 取得消息队列中上一条消息处理完毕时的鼠标指针屏幕位置 GetMessageTime 取得消息队列中上一条消息处理完毕时的时间 PostMessage 将一条消息投递到指定窗口的消息队列 PostThreadMessage 将一条消息投递给应用程序 RegisterWindowMessage 获取分配给一个字串标识符的消息编号 ReplyMessage 答复一个消息 SendMessage 调用一个窗口的窗口函数,将一条消息发给那个窗口 SendMessageCallback 将一条消息发给窗口 SendMessageTimeout 向窗口发送一条消息 SendNotifyMessage 向窗口发送一条消息 3. API之文件处理函数 CloseHandle 关闭一个内核对象。其中包括文件、文件映射、进程、线程、安全和同步对象等 CompareFileTime 对比两个文件的时间 CopyFile 复制文件 CreateDirectory 创建一个新目录 CreateFile 打开和创建文件、管道、邮槽、通信服务、设备以及控制台 CreateFileMapping 创建一个新的文件映射对象 DeleteFile 删除指定文件 DeviceIoControl 对设备执行指定的操作 DosDateTimeToFileTime 将DOS日期和时间值转换成一个 win32 FILETIME 值 FileTimeToDosDateTime 将一个 win32 FILETIME 值转换成DOS日期和时间值 FileTimeToLocalFileTime 将一个FILETIME结构转换成本地时间 FileTimeToSystemTime 根据一个FILETIME结构的内容,装载一个SYSTEMTIME结构 FindClose 关闭由FindFirstFile函数创建的一个搜索句柄 FindFirstFile 根据文件名查找文件 FindNextFile 根据调用FindFirstFile函数时指定的一个文件名查找下一个文件 FlushFileBuffers 针对指定的文件句柄,刷新内部文件缓冲区 FlushViewOfFile 将写入文件映射缓冲区的所有数据都刷新到磁盘 GetBinaryType 判断文件是否可以执行 GetCompressedFileSize 判断一个压缩文件在磁盘上实际占据的字节数 GetCurrentDirectory 在一个缓冲区中装载当前目录 GetDiskFreeSpace 获取与一个磁盘的组织有关的信息,以及了解剩余空间的容量 GetDiskFreeSpaceEx 获取与一个磁盘的组织以及剩余空间容量有关的信息 GetDriveType 判断一个磁盘驱动器的类型 GetExpandedName 取得一个压缩文件的全名 GetFileAttributes 判断指定文件的属性 GetFileInformationByHandle 这个函数提供了获取文件信息的一种机制 GetFileSize 判断文件长度 GetFileTime 取得指定文件的时间信息 GetFileType 在给出文件句柄的前提下,判断文件类型 GetFileVersionInfo 从支持版本标记的一个模块里获取文件版本信息
汇编语言教程 想了解机器底层运作的话就认真看看吧 课程介绍 第1章 预备知识  1.1 汇编语言的由来及其特点   1 机器语言   2 汇编语言   3 汇编程序   4 汇编语言的主要特点   5 汇编语言的使用领域  1.2 数据的表示和类型   1 数值数据的表示   2 非数值数据的表示   3 基本的数据类型  1.3 习题 第2章 CPU资源和存储器  2.1 寄存器组   1 寄存器组   2 通用寄存器的作用   3 专用寄存器的作用  2.2 存储器的管理模式   1 16位微机的内存管理模式   2 32位微机的内存管理模式  2.3 习题 第3章 操作数的寻址方式  3.1 立即寻址方式  3.2 寄存器寻址方式  3.3 直接寻址方式  3.4 寄存器间接寻址方式  3.5 寄存器相对寻址方式  3.6 基址加变址寻址方式  3.7 相对基址加变址寻址方式  3.8 32位地址的寻址方式  3.9 操作数寻址方式的小结  3.10 习题 第4章 标识符和表达式  4.1 标识符  4.2 简单内存变量的定义   1 内存变量定义的一般形式   2 字节变量   3 字变量   4 双字变量   5 六字节变量   6 八字节变量   7 十字节变量  4.3 调整偏移量伪指令   1 偶对齐伪指令   2 对齐伪指令   3 调整偏移量伪指令   4 偏移量计数器的值  4.4 复合内存变量的定义   1 重复说明符   2 结构类型的定义   3 联合类型的定义   4 记录类型的定义   5 数据类型的自定义  4.5 标号  4.6 内存变量和标号的属性   1 段属性操作符   2 偏移量属性操作符   3 类型属性操作符   4 长度属性操作符   5 容量属性操作符   6 强制属性操作符   7 存储单元别名操作符  4.7 表达式   1 进制伪指令   2 数值表达式   3 地址表达式  4.8 符号定义语句   1 等价语句   2 等号语句   3 符号名定义语句  4.9 习题 第5章 微机CPU的指令系统  5.1 汇编语言指令格式   1 指令格式   2 了解指令的几个方面  5.2 指令系统   1 数据传送指令   2 标志位操作指令   3 算术运算指令   4 逻辑运算指令   5 移位操作指令   6 位操作指令   7 比较运算指令   8 循环指令   9 转移指令   10 条件设置字节指令   11 字符串操作指令   12 ASCII-BCD码调整指令   13 处理器指令  5.3 习题 第6章 程序的基本结构  6.1 程序的基本组成   1 段的定义   2 段寄存器的说明语句   3 堆栈段的说明   4 源程序的结构  6.2 程序的基本结构   1 顺序结构   2 分支结构   3 循环结构  6.3 段的基本属性   1 对齐类型   2 组合类型   3 类别   4 段组  6.4 简化的段定义   1 存储模型说明伪指令   2 简化段定义伪指令   3 简化段段名的引用  6.5 源程序的辅助说明伪指令   1 模块名定义伪指令   2 页面定义伪指令   3 标题定义伪指令   4 子标题定义伪指令  6.6 习题 第7章 子程序和库  7.1 子程序的定义  7.2 子程序的调用和返回指令   1 调用指令   2 返回指令  7.3 子程序的参数传递   1 寄存器传递参数   2 存储单元传递参数   3 堆栈传递参数  7.4 寄存器的保护与恢复  7.5 子程序的完全定义   1 子程序完全定义格式   2 子程序的位距   3 子程序的语言类型   4 子程序的可见性   5 子程序的起始和结束操作   6 寄存器的保护和恢复   7 子程序的参数传递   8 子程序的原型说明   9 子程序的调用伪指令   10 局部变量的定义  7.6 子程序库   1 建立库文件命令   2 建立库文件举例   3 库文件的应用   4 库文件的好处  7.7 习题 第8章 输入输出和中断  8.1 输入输出的基本概念   1 I/O端口地址   2 I/O指令  8.2 中断   1 中断的基本概念   2 中断指令   3 中断返回指令   4 中断和子程序  8.3 中断的分类   1 键盘输入的中断功能   2 屏幕显示的中断功能   3 打印输出的中断功能   4 串行通信口的中断功能   5 鼠标的中断功能   6 目录和文件的中断功能   7 内存管理的中断功能   8 读取和设置中断向量  8.4 习题 第9章 宏  9.1 宏的定义和引用   1 宏的定义   2 宏的引用   3 宏的参数传递方式   4 宏的嵌套定义   5 宏与子程序的区别  9.2 宏参数的特殊运算符   1 连接运算符   2 字符串整体传递运算符   3 字符转义运算符   4 计算表达式运算符  9.3 与宏有关的伪指令   1 局部标号伪指令   2 取消宏定义伪指令   3 中止宏扩展伪指令  9.4 重复汇编伪指令   1 伪指令REPT   2 伪指令IRP   3 伪指令IRPC  9.5 条件汇编伪指令   1 条件汇编伪指令的功能   2 条件汇编伪指令的举例  9.6 宏的扩充   1 宏定义形式   2 重复伪指令REPEAT   3 循环伪指令WHILE   4 循环伪指令FOR   5 循环伪指令FORC   6 转移伪指令GOTO   7 宏扩充的举例   8 系统定义的宏  9.7 习题 第10章 应用程序的设计  10.1 字符串的处理程序  10.2 数据的分类统计程序  10.3 数据转换程序  10.4 文件操作程序  10.5 动态数据的编程  10.6 COM文件的编程  10.7 驻留程序  10.8 程序段前缀及其应用   1 程序段前缀的字段含义   2 程序段前缀的应用  10.9 习题 第11章 数值运算协处理器  11.1 协处理器的数据格式   1 有符号整数   2 BCD码数据   3 浮点数  11.2 协处理器的结构  11.3 协处理器的指令系统   1 操作符的命名规则   2 数据传送指令   3 数学运算指令   4 比较运算指令   5 超越函数运算指令   6 常数操作指令   7 协处理器控制指令  11.4 协处理器的编程举例  11.5 习题 第12章 汇编语言和C语言  12.1 汇编语言的嵌入  12.2 C语言程序的汇编输出  12.3 一个具体的例子  12.4 习题 附录
摘自:http://mbstudio.spaces.live.com/blog/cns!C898C3C40396DC11!955.entry 2007/1/30 oSIP协议栈(及eXoSIP,Ortp等)使用入门(原创更新中) (CopyLeft by Meineson | www.mbstudio.cn,原创文章,欢迎转载,但请保留出处说明!) 本文档最新版本及文中提到的相关源码及VC6工程文件请在本站找,嘿嘿~~ (首页的SkyDriver公开文件夹中,可能需要用代理才能正常访问该空间——空间绝对稳定,不会丢失文件!) (最近工作重心不在SIP开发,SO本文档也没有机会更新,有技术问题也请尽量咨询他人,本人不一定能及时回复。)   一直没空仔细研究下oSIP,最近看到其版本已经到了3.x版本,看到网上的许多帮助说明手册都过于陈旧,且很多文档内容有点误人子弟的嫌疑~~   Linux下oSIP的编译使用应该是很简单的,其Install说明文档里也介绍的比较清楚,本文主要就oSIP在Windows平台下VC6.0开发环境下的使用作出描述。   虽然oSIP的开发人员也说明了,oSIP只使用了标准C开发库,但许多人在Windows下使用oSIP时,第一步就被卡住了,得不到oSIP的LIB库和DLL库,也就没有办法将oSIP使用到自己的程序中去,所以第一步,我们将学习如何得到oSIP的静态和动态链接库,以便我们自己的程序能够使用它们来成功编译和执行我们的程序。 第一阶段: ------------------------------------------------------   先创建新工程,网上许多文档都介绍创建一个Win32动态链接库工程,我们这里也一样,创建一个空白的工程保存。   同样,将oSIP2版本3.0.1 src目录下的Osipparser2目录下的所有文件都拷到我们刚创建的工程的根目录下,在VC6上操作: Project-Add To Project-Files   将所有的源程序和头文件都加入到工程内,保存工程。   这时,我们可以尝试编译一下工程,你会得到许多错误提示信息,其内容无非是找不到osipparser2/xxxxx.h头文件之类。   处理:在Linux下,我们一般是将头文件,lib库都拷到/usr/inclue;/usr/lib之类的目录下,c源程序里直接写#include 时,能直接去找到它们,在VC里,同样的,最简单的方法就是将oSIP2源码包中的Include目录下的 osipparser2目录直接拷到我们的Windows下默认包含目录即可,这个目录在VC6的Tool-Options-Directories里设置,(当然,如果你知道这一步,也可以不用拷贝文件,直接在这里把oSIP源码包所在目录加进来就可以了),默认如果装在C盘,目录则为 C:\Program Files\Microsoft Visual Studio\VC98\Include。   这时,我们再次编译我们的工程,顺利编译,生成osipparser2.dll,这时,网上很多文档里可能直接就说,这一步也会生成libs目录,里面里osipparser2.lib文件,但我们这里没有生成:)   最简单的方法,不用深究,直接再创建一个工程,同上述创建动态链接库方法,创建一个Win32静态链接库工程,直接编译,即可得到osipparser2.lib。 ------------------------------------------------------   上面,我们得到了Osip的解析器开发库,下面再编译完整的Osip协议栈开发库,同样照上述方法,分别创建动态链接库工程和静态链接库工程,只是要拷的文件换成src下的osip目录下文件和include下的osip目录,得到osip2.dll和osip2.lib。   在编译osip2.dll这一步可能会再次得到错误,内容含义是找不到链接库,所以,我们要把前面编译得到的osipparser2.lib也拷到osip工程目录下,并在VC6中操作:   Project-Setting-Link中的Object/Library Modules: kernel32.lib user32.lib ... xxx.lib之类的内容最后增加: osipparser2.lib   保存工程后再次编译,即可成功编译osip2.dll。 ------------------------------------------------------   至此,我们得到了完整的oSIP开发库,使用时,只需在我们的程序里包含oSIP的头文件,工程的链接参数里增加osipparser2.lib和osip2.lib即可。 ------------------------------------------------------   下面我们验证一下我们得到的开发库,并大概了解一下OSIP的语法规范。   在VC里创建win32控制台程序工程,将libosip源码包的SRC目录下的Test目录内的C源程序随便拷一个到工程时,直接编译(工程设置里照前文方法在link选项里增加osip2.lib,osipparser2.lib引用我们之前成功编译得到的静态库文件)就可以运行(带参数运行,参数一般为一个文本文件,同样从Test目录的res目录里拷一个与源文件同名的纯文本文件到工程目录下即可)。   该目录下的若干文件基本上是测试了Osip的一些基本功能函数,例如URI解析之类,可以大概了解一下oSIP的语法规范和调用方法,同时也能校验一下之前编译的OSIP开发库能否正常使用,成功完成本项工作后,可以进入下一步具体的oSIP的使用学习了。 ------------------------------------------------------   由于oSIP是比较底层的SIP协议栈实现,新手较难上手,而官方的示例大都是一些伪代码,需要有实际的例子程序参考学习,而最好的例子就是同样官方发布的oSIP的扩展开发库exosip2,使用exoSIP可以很方便地快速创建一个完整的SIP程序(只针对性地适用于SIP终端开发用,所以我们这里只是用它快速开发一个SIP终端,用来更方便地学习oSIP,要想真正掌握SIP的开发,需要掌握oSIP并熟读RFC文档才行,exoSIP不是我们的最终学习目的),通过成功编译运行一个自己动手开发出的程序,再由浅入深应该是初学都最好的学习方法通过对使用exosip开发库的使用创建自己的SIP程序,熟悉后再一个函数一个函数地深入学习exosip提供的接口函数,就可以深入理解osip 了,达到间接学习oSIP的目的,同时也能从eXoSIP中学习到正确使用oSIP的良好的编程风格和语法格式。   而要成功编译ExoSIP,似乎许多人被难住了,直接在XP-sp2上,用VC6,虽然你使用了eXoSIP推荐的winsock2.h,但是会得到一个 sockaddr_storage结构不能识别的错误,因为vc6自带的开发库太古董了,需要升级系统的Platform SDK,下载地址如下: http://www.microsoft.com/msdownl ... PSP2FULLInstall.htm(VC6的支持已经停止,这是VC6能使用的最新SDK)   成功安装后编译前需加OSIP_MT宏,以启用线程库,否则在程序中使用eXoSIP库时会出错,而编译时也会得到许多函数未定义的Warning提示,编译得到exosip2.lib供我们使用,当然,在此之前需要成功编译了osip2和osipparser2,而在之后的实际使用时,发现oSIP也需要增加OSIP_MT宏,否则OSIP_MT调用oSIP的线程库时会出错,所以我们需要重新编译oSIP了:),因为eXosip是基于oSIP的(同上方式创建静态和动态链接库工程,并需在Link中手工添加oSIP和oSIPparser的lib库)。 ------------------------------------------------------   创建新工程,可以是任意工程,我们从最简单的Win32控制台程序开始,为了成功使用oSIP,我们需要引用相关库,调用相关头文件,经过多次试验,发现需要引用如下的库: exosip2.lib osip2.lib osipparser2.lib WSock32.Lib IPHlpApi.Lib WS2_32.Lib Dnsapi.lib   其中,除了我们上面编译得到的三个oSIP库外,其它库都是系统库,其中有一些是新安装的Platform SDK所新提供的。   至此,我们有了一个简单的开发环境了,可以充分利用网上大量的以oSIP为基础的代码片段和官方说明文档开始具体函数功能的测试和使用了:) ------------------------------------------------------   我们先进行一个简单的纯SIP信令(不带语音连接建立)的UAC的SIP终端的程序开发的试验(即一个只能作为主叫不能作为被叫的的SIP软电话模型),我们创建一个MFC应用程序,对话框模式,照上面的说明,设置工程包含我们上面得到的oSIP的相关开发库及SDK的一些开发库,并且由于默认LIBC的冲突,需要排除MSVCRT[D]开发库(其中D代表Debug模式下,没有D表示Release模式下),直接使用eXosip的几个主要函数就可以创建一个基本的SIP软电话模型。   其主要流程为:   初始化eXosip库-启动事件监听线程-向SIP Proxy注册-向某SIP终端(电话号码)发起呼叫-建立连接-结束连接   初始化代码: int ret = 0; ret = eXosip_init (); eXosip_set_user_agent("##YouToo0.1"); if(0 != ret) { AfxMessageBox("Couldn't initialize eXosip!\n"); return false; } ret = eXosip_listen_addr (IPPROTO_UDP, NULL, 0, AF_INET, 0); if(0 != ret) { eXosip_quit (); AfxMessageBox("Couldn't initialize transport layer!\n"); return false; }   启动事件监听线程: AfxBeginThread(sip_uac,(void *)this);   向SIP Proxy注册: eXosip_clear_authentication_info(); eXosip_add_authentication_info(uname, uname, upwd, "md5", NULL); real_send_register(30);  /* 自定义函数代码请见源码 */   发起呼叫(构建假的SDP描述,实际软电话使用它构建RTP媒体连接): osip_message_t *invite = NULL; /* 呼叫发起消息体 */ int i = eXosip_call_build_initial_invite (&invite, dest_call, source_call, NULL, "## YouToo test demo!"); if (i != 0) { AfxMessageBox("Intial INVITE failed!\n"); } char localip[128]; eXosip_guess_localip (AF_INET, localip, 128); snprintf (tmp, 4096, "v=0\r\n" "o=josua 0 0 IN IP4 %s\r\n" "s=conversation\r\n" "c=IN IP4 %s\r\n" "t=0 0\r\n" "m=audio %s RTP/AVP 0 8 101\r\n" "a=rtpmap:0 PCMU/8000\r\n" "a=rtpmap:8 PCMA/8000\r\n" "a=rtpmap:101 telephone-event/8000\r\n" "a=fmtp:101 0-11\r\n", localip, localip, "9900"); osip_message_set_body (invite, tmp, strlen(tmp)); osip_message_set_content_type (invite, "application/sdp"); eXosip_lock (); i = eXosip_call_send_initial_invite (invite); eXosip_unlock ();   挂断或取消通话: int ret; ret = eXosip_call_terminate(call_id, dialog_id); if(0 != ret) { AfxMessageBox("hangup/terminate Failed!"); }   可以看到非常简单,再借助于oRTP和Mediastreamer开发库,来快速为我们的SIP软电话增加RTP和与系统语音API接口交互及语音编码功能,即可以快速开发出一个可用的SIP软电话,关于oRTP和Mediastreamer的相关介绍不是本文重点,将在有空的时候考虑增加相应使用教程,文章前提到的地方可以下载基本可用的完整SIP软电话的VC源码工程文件供参考使用,完全CopyLeft,欢迎转载,但请在转载时注明作者信息,谢谢! 第二阶段: ---------------------------------------------------   得到了一个SIP软电话模型后,我们可以根据软电话的实际运行表现(结合用Ethereal抓包分析)来进行代码的分析,以达到利用eXoSIP来辅助我们学习oSIP的最终目的(如要快速开发一个可用的SIP软电话,请至前面提到的论坛去下载使用oRTP和Mediastreamer快速搭建的一个基本完整可用的SIP软电话##YouToo 0.1版本的VC源码工程文件作参考)。   现在从eXosip的初始化函数开始入手,来分析oSIP的使用,这是第二阶段,第三阶段就是深入学习oSIP的源码了,但大多数情况下应该没有必要了,因为在第二阶段就有部分涉及到第三阶段的工作了,而且oSIP的源码也就大多是一些SIP数据的语法解析和状态机的实现,能深入理解了SIP协议后,这些只是一种实现方式,没必要完全去接受,而是可以用自己的方式和风格来实现一套,比如,更轻量化更有适用目的性的方式,oSIP则只起参考作用了。   eXosip_init()是eXosip的初始化函数,我们来看看它的内部实现:   首行是定义的 osip_t *osip,这在oSIP的官方手册里我们看到,所有使用oSIP的程序都要在最开始处声明一个osip_t的指针,并使用 osip_init(&osip)来初始化这个指针,销毁这个资源使用osip_release(osip)即可。   我们可以在代码中看到很多OSIP_TRACE,这是调试输出宏调用了函数osip_trace,可以用ENABLE_TRACE宏来打开调试以方便我们开发调试。   其它就是很多的eXosip_t的全局变量eXosip的一些初始化操作,包括最上面的memset (&eXosip, 0, sizeof (eXosip))完全清空和下面的类似eXosip.user_agent = osip_strdup ("eXosip/" EXOSIP_VERSION)的exosip变量的一些初始值设置,其中有一个eXosip.j_stop_ua = 0应该是一个状态机开关,后面可以看到很多代码检测这个变量来决定是否继续流程处理,默认置成了0表示现在exosip的处理流程是就绪的,即ua是 not stop的。      osip_set_application_context (osip, &eXosip)是比较有意思的,它让下面的eXosip_set_callbacks (osip)给osip设置大量的回调函数时,能让osip能访问到eXosip这个全局变量中设置的大量程序运行时交互的信息,相当于我们在VC下开启一个线程时,给线程传入的一个void指针指向我们的MFC应用程序的当前dialog对象实例,可以用void *osip_get_application_context (osip_t * osip)这个函数来取出指针来使用,不过好象exosip中并没有用到它,可能是留给个人自已扩展的吧:)      还能看到初始化代码前面有一段WIN32平台下的SOCK的初始化代码,可以知道eXosip是用的原生的winsock api函数,也就是我们可能以前学过的用VC和WINAPI写sock程序时(不是MFC),用到的那段SOCK初始代码,还有一段有意思的代码,就是 jpipe()函数,它们返回的是一个管道,一个有2个整型数值的数组(一个进一个出),查看其代码发现,非WIN32平台是直接使用的pipe系统函数,而WIN32下则是用一对TCP的本地SOCK连接来模拟的管道,一个SOCK写一个SOCK读,这段代码是比较有参考价值的:) j = 50; while (aport++ && j-- > 0) {   raddr.sin_port = htons ((short) aport);   if (bind (s, (struct sockaddr *) &raddr, sizeof (raddr)) transactionid)); }   即,只是打印一下调试,并没有完整实现什么功能,我们学习时,完全可以用相同的方法,定义一大堆回调函数,并不忙想怎么完全实现,先都是只打印一下调试信息,看具体的应用逻辑根据抓包测试分析和看调试看程序走到了哪一步,调用了哪一个回调,来明白具体回调函数要实现什么用途,再来实现代码就方便多了,当然,如果看透了RFC文档,应该从字面就能知道各个回调函数的用途了,这是后话,不是谁都能快速完全看懂RFC的,所以我们要参考eXosip:)      我们对其中的重要的回调函数进行逐个的分析:   ---------------------------   osip_set_cb_send_message (osip, &cb_snd_message) SIP消息发送回调函数   这个函数可能是最重要的回调函数之一,消息发送,包括请求消息和回应消息,一般情况下,状态机的状态就是由它控制的,发起一个消息初始化一个状态机,回应一个消息对状态机修改,终结消息发送结束状态机……   看cb_snd_message的函数实现,要以发现,其主要代码是对参数中的要发送的消息osip_message_t * sip进行分析,找出消息要发送的真实char *host,int port的值(这些参数可以省略,但要发送消息肯定需要host和port,所以要从sip中解析),最后根据sip中解析出的传输方式是TCP还是 UDP选择最终进行消息发送处理的函数cb_udp_snd_message,cb_tcp_snd_message处理(它们的参数一致,即本函数只是补全一些省略的参数并对消息进行合法性检查)。   **毕竟eXosip是一个通用的开发库,它考虑了要支持TCP,UDP,TCPs,IPV4,IPV6,WIN32,*nix,WINCE等等多样化的复杂环境,所以,我们可以略过我们暂时不需要的部分,比如,IPV6相关的代码实现等。      由于我们大多数情况下SIP是用的UDP,所以先来看一下cb_udp_snd_message的实现,它从全局变量exosip中获取可用的 sock,并尽最大能力解析出host和port(??难道前面的函数还不够解析彻底??如最终仍无port信息则默认设置为5060),使用 osip_message_to_str (sip, &message, &length)函数将要发送的格式化的SIP消息转换成能用SOCK传输的简单数据并发送即完成消息发送,代码中有许多复杂的环境探测和错误控制等等等等,我们可以暂时不用过多关注,可以继续向下,结尾处有一个keeplive相关代码,从代码字面分析,可能是SIP的Register消息的自动重发相关代码,可以在后面再细化分析。   cb_tcp_snd_essage的函数实现要比上文的udp的实现简单很多,主要是环境探测错误控制方面,因为毕竟tcp是稳定连接的,对比一下代码,可以看到主要流程还是将SIP消息转换后,发送到从SIP消息中解析出的host和port对应的目标。      看完两个函数,可以知道,eXosip需要有两个sock,是一个数组,0是给UDP用的,1是给TCP用的,要用SOCK当然要初始化,就是下文要介绍的eXosip的网络相关的初始化了,上面的exosip_init可以看成是这个开发库的系统初始化吧:)    至些,我们应该知道了oSIP开发的SIP应用程序的消息是从哪里发出的吧,对了,就是从这个回调函数里,所谓万事开头难,就象开发WIN32应用程序时,找到了WIN32程序的main函数入口下面的工作就好办了,下面就都是为一些事件消息开发对应的处理函数而已了:)   osip_set_kill_transaction_callback 事务终结回调函数   对应ICT,IST,NICT,NIST客户/服务器注册/非注册事务状态机的终结,主要是使用osip_remove_transaction (eXosip.j_osip, tr)将当前tr事务删除,再加上一系列的清理工作,其中,NICT即客户端的非Invite事务的清理比较复杂一些,要处理的内容也比较多,可以根据实际应用的情况进行有必要的清理工作:)   cb_transport_error 传输失败处理回调   对应于上面说到的四种事务状态机,如果它们在处理时失败,则在这时进行统一处理。   从代码可知,只是在NOTIFY,SUBSCRIBE,OPTION操作失败才进行处理,其它错误可直接忽略。   osip_set_message_callback 消息发送处理回调   根据type不同,表示不同的消息发送状态   OSIP_XXX_AGAIN 重发相关消息   OSIP_ICT_INVITE_SENT 发起呼叫   OSIP_ICT_ACK_SENT ACK回应   OSIP_NICT_REGISTER_SENT 发起注册   OSIP_NICT_BYE_SENT BYE发出   OSIP_NICT_CANCEL_SENT Cancel发出   OSIP_NICT_INFO_SENT,OSIP_NICT_OPTIONS_SENT,OSIP_NICT_SUBSCRIBE_SENT,OSIP_NICT_NOTIFY_SENT,OSIP_NICT_UNKNOWN_REQUEST_SENT   我们可以看到,eXosip没有对它们作任何处理,我们可以根据自己需要,比如,重发2xx消息前记录一下日志之类的,扩展一下retransmission的处理方式,发起Invite前记录一下通话日志等等。   OSIP_ICT_STATUS_1XX_RECEIVED uac收到1xx消息,一般是表示对端正在处理中,这时,主要是设置一下事务状态机的状态值,并对会话中的osip的一些参数根据返回值进行相应设置,里面有许多条件判断,但我们常用的一般是100,180,183的判断而已,暂时可以忽略里面复杂的判断代码。   OSIP_ICT_STATUS_2XX_RECEIVED uac收到2xx消息,这里主要跟踪一下Register情况下的2xx,表示注册成功,这时会更新一下exosip的注册字段值,以便让eXosip能自动维护uac的注册,BYE的2xx回应是终结消息,Invite的2xx回应,则主要是初始化一下会话相关的数据,表示已成功建立连接。   其它4xx,5xx,6xx则分别是对应的处理,根据实现情况进行概要的查看即可。   report_event (je, sip)是代码中用来进行事件处理的一个函数,跟踪后发现,其最终是使用了我们上文提到的jpipe管道,以便在状态机外实时观测状态机内的处理信息。      OSIP_NIST_STATUS_XXX_SENT即对应于上面的uac的处理,这里是uas的对应的消息处理,相比较于uac简单一点。   前面简单介绍了一下大量的回调函数及它们的概要处理逻辑,可能会比较混乱,暂时不用管它,只需要记得一个大概的形象,知道一个SIP处理程序是通过osip_set_cb_send_message回调函数来实现真实地发送各种SIP消息,并且SIP的标准事务模型是由oSIP实现好了,我们只需要给不同的事务状态设置不同的回调处理函数来处理事务,具体的状态变化和内部逻辑不用管就可以了。   下面来说一下消息处理回调函数用到的SOCK的初始化函数,即我们上面说的除了系统初始化外的网络初始化函数eXosip_listen_addr:   从上文知道了,系统将初始化两个SOCK,一个UDP一个TCP,但查看代码发现还有第三个,TCPs的,但好象还不能实用,现在不管它,代码首先是根据传输是UDP还是TCP来设置对应的数组值,并且如果没有提供IP地址和端口号,系统会自动取出本机网络接口并创建可用的SOCK(http_port 的方式暂不用考虑)。   SOCK初始化后,如何开始SIP事务的呢?看到这个调用eXosip.j_thread = (void *) osip_thread_create (20000, _eXosip_thread, NULL),对的,这里启用了一个线程,即,eXosip是调用oSIP的线程函数(没用系统提供的线程函数,是为了跨平台)进行事务处理的状态机逻辑是在一个线程中处理的,这样就明白了为什么一直没能看到顺序执行下来的程序启动代码了,接下去看,线程实际处理函数是_eXosip_thread,这里面的代码中,我们看到了上文提到的状态机控制开关变量while (eXosip.j_stop_ua == 0),即,当j_stop_ua设置为1时,osip_thread_exit ()结束事务处理即程序终结,再接下去看,_eXosip_execute是最终的处理函数了,而且它在程序未终结情况下是一直逻辑在执行,注意,要启用oSIP的多线程宏OSIP_MT。      看到_eXosip_execute的代码中有很多时间函数和变量,仔细看,除去一些控制代码,主要处理函数是eXosip_read_message (1, lower_tv.tv_sec, lower_tv.tv_usec),即取出消息,1表示只取出一条消息,其代码量非常的大,但同样的,其中也许多的控制代码和错误检测代码,我们在查看时可以暂时忽略掉它们。   eXosip_read_message读取消息时,即没有采用sock的block也没有用非block方式,而是采用了select方式,具体应用可查询fd_set相关文档。   根据jpipe_read (eXosip.j_socketctl, buf2, 499),我们可以估计,buf2中应该是保存的我们的控制管道的数据,具体作用至些还没有表现出来,应该是用来反映一些状态机内部的警示之类的信息,实际的SIP的处理的状态机的数据是存放在buf中,使用_eXosip_recvfrom获取的,获取后sipevent = osip_parse (buf, i)解析,使用osip_find_transaction_and_add_event (eXosip.j_osip, sipevent)来查询事件对应的事务状态机,找到后就如同其注解所说明的,/* handled by oSIP ! */,即我们上文设置的那一大堆回调函数,至此,我们知道了整个SIP应用所处理的大概流程了。   如果没有找到事务状态机呢?直接丢弃吗?不是的,如果这是一个回应消息,但没有事务状态机处理它,那它是一个错误的,要进行清理后才能丢弃,而如果是一个请求,那更不能丢弃了,因为UAS事务状态机要由它来启动创建的(回应消息表示本地发出了请求消息,即UAC行为,事务状态机应是由启动UAC的代码初始化启动的),整个逻辑应该是很简单的,但eXosip的实现代码却非常多,可见其花了非常多的精力在保证会话的稳定性和应付网络复杂情况上,我们可以对其进行大量的精简来构建满足我们需求的代码实现。   先来看错误的回应消息的处理函数eXosip_process_response_out_of_transaction,可以看到其代码就是一大堆的赋值语句,XXX= NULL,即将一大堆的运行时变量清空,再调用osip_event_free清空事件,或者就是一些复杂的情况下,需要通过解析现在的运行时数据,从中分析出“可能”的正在等待回应的对端,并发送相关终结通知消息等等,可以根据实际需要进行简化。   请求事件的处理 eXosip_process_newrequest,首先是对事件进行探测,MSG_IS_INVITE、MSG_IS_ACK、 MSG_IS_REQUEST……,对事件进行所属状态机分类,随后使用_eXosip_transaction_init (&transaction,(osip_fsm_type_t) tx_type,eXosip.j_osip, evt->sip)根据探测结果进行状态机初始化,实际调用的是osip_transaction_init,初始化后即将事件入状态机 osip_transaction_add_event (transaction, evt),由状态机自动处理后调用相应回调函数处理逻辑了。当然,eXosip为方便快速开发SIP终端应用,在下面又添加了许多自动化的处理代码,来和我们在回调函数中设置的处理代码相区分。   线程调用的事件处理函数代码最后是 if (eXosip.keep_alive > 0) {   _eXosip_keep_alive (); }   这段代码印证了上文提到了,keep_alive是用来设置是否自动重新注册,由_eXosip_keep_alive函数来实现自动将eXosip全局变量中保存的注册消息解析后自动根据需要重新向SIP服务器发起Register注册。   同样,因为注册消息发起是UAC的行为,将它放在这里,可以看出来所有事件消息的事务状态机处理都是在这里,只不过这里只创建UAS的事务状态机,UAC的事务状态机的创建则要继续到下面找了,从我们的YouToo软电话代码中可知,发起呼叫和发起注册分别调用了 eXosip_call_send_initial_invite,eXosip_register_send_register这两个函数(另外用到的两个build函数则是分别构建这两个send函数要发送的SIP消息),查看这两个函数可知,UAC的事务处理状态机是在这里进行初始化的。   eXosip_register_send_register中可以看到是_eXosip_transaction_init (&transaction, NICT, eXosip.j_osip, reg)初始化UAC状态机,实际也同UAS是调用的osip_transaction_init函数,同样使用 osip_transaction_add_event (transaction, sipevent)将事件入状态机,状态机随后将自动处理调用相应回调函数处理逻辑了。   另有osip_new_outgoing_sipmessage(reg),表示发送消息,到这里,我们应该可以理解,真实的发送操作,是要到由状态机处理后,调用了消息发送回调函数才真正地将注册消息发送出去的。   同注册消息发送,它是NICT状态机,呼叫消息的发送是ICT,由eXosip_call_send_initial_invite处理,_eXosip_transaction_init (&transaction, ICT, eXosip.j_osip, invite)初始化了状态机,之前还有一个eXosip_call_init是用来初始化eXosip的一些参数的,暂时不管它,同样 osip_new_outgoing_sipmessage (invite)发送呼叫消息,但实际还是要状态机处理后调用消息发送回调函数真实发送呼叫请求函数的,osip_transaction_add_event (transaction, sipevent)则标准地,将事件入状态机,状态机将能处理随后的应用逻辑调用相应的回调函数了。   好了,作了这么多的分析,我们了解了eXosip是怎样调用oSIP来形成被我能方便地再次调用的了,可以看到,为了实现最大限度的跨平台和兼容性,代码中有大量的测试代码,宏定义和错误再处理代码,看起来非常吃力,但了解了其主要的调用框架:   初始化,回调函数设置,UAC和UAS事务处理状态机的启动,事件处理流程等,就可以基本明白了oSIP各个函数的主要作用和正确的用法了,下一步,可以参考eXosip来针对某个应用,去除掉大量暂时用不到的代码,来构建一个简单的SIP软电话和SIP服务器,来进一步深入oSIP学习应用了。  ------------------------------------------------------ [下回预告:完全基于oSIP的软电话实现及oSIP进一步学习] (CopyLeft by Meineson | www.mbstudio.cn,原创文章,欢迎转载,但请保留出处说明!) 附件为原作者提供的
这是一个通用报表引擎,构想完成的功能有: 1、提供给软件开发商一个报表开发中间件。针对c/s平台,用户可以以SDK的方式或Com的方式将其加入到自己的产品中。 针对b/s平台,该中间件通过在应用服务器端嵌入服务以处理WEB报表请求同时返回HTML页面。C/S架构中能够非常方便 的展示、打印、自动分页等功能,可以通过配置文件配置一张报表。支持复杂的财务报表。 2、可以作为一个通用的外挂式报表系统终端,可以在设计器中设计报表的取数规则,这些规则包括数据来源的数据库、数据 表以及相应的取数条件,这些规则会被报表底层解析成特定SQL语句并执行。最后将返回的数据直接按照配置的数据展示 逻辑展示在报表设计器中。 3、可以作为一般通用的表格应用系统被集成到应用软件中,以提供类似于Excel的部分基本表格功能。 以下为软件架构及开发现状的一点说明: 目前,报表底层大部分功能已经完成。Demo展示的报表设计器由于时间比较仓促部分底层实现的基本功能还没能集成进去, 所以此Demo程序仅能展示部分功能。同时由于方便打包及展示,我特意将报表底层部件静态编译到了设计器中。这样演示的 时候作为一个“绿色软件”可以不基于其他支持库而运行。真正的报表底层包括以下几个Dll: 1、LTableLib.dll (提供表格的基本逻辑) 2、LGridLib.dll (提供数据表的一些增强功能) 3、LSheetLib.dll (提供类似于Excel中工作表的功能) 4、LGUILib.dll (提供表格绘制功能:为了兼容于非Windows系统(如Linux) ,特将系统相关的展现接口独立出来。可以非常方便的将本系统移植到非Windows系统中) 5、LGridBook.dll(封装类似于Excel中工作簿(Book)功能,本Dll为提供用户SDK开发的最终接口) 6、(可选)CLGridBook.dll(为了兼容其他支持Com标准的非c/c++开发工具(如:VB、VF、Delphi等),特意将底层封装 为一个标准Com组件,可以更加方便的提供支持) 最后,为了支持后续的功能扩展,系统设计成了一个开放性系统。新增的功能可以通过实现系统提供的各种插件接口进行任意扩展(如 Demo中插入图片的功能就是其中一个例子,另外艺术字及图表功能可以通过类似方式提供支持)。在可移植性方面,由于所有的底层代码 均采用标准C++编写,同时有效的将操作系统相关的技术进行了抽象(如展现GUI、事件、定时器、同步对象等)。在移植的时候只需要 重新实现几个简单的接口就可以了。 Demo中LRptDemo.etl文件是用设计器设计的一个报表样式,可以通过设计器将此文件打开展示。由于开发设计器的时间比较仓促,所以很多 菜单功能以及部分工具按钮功能还没有加进去(尽管这些功能以及被底层所支持)。 另:报表类似于Excel的公式及常用函数支持正在编写中,已实现的功能也不可避免的存在或多或少的BUG,见笑了 :) 如需要SDK/或COM开发包可以发邮件给我。呵呵:) E-Mail:[email protected]
### 回答1: 以下是代码实现: ```csharp using System; class MainClass { public static void Main (string[] args) { Console.Write("请输入一个整数:"); int x = int.Parse(Console.ReadLine()); for (int i = 1; i <= x; i++) { for (int j = 1; j <= 2 * i - 1; j++) { Console.Write("*"); } Console.WriteLine(); } } } ``` 运行结果: ``` 请输入一个整数:6 * *** ***** ******* ********* *********** ``` ### 回答2: 使用C语言可以实现以下功能: 1. 开发操作系统:C语言是开发操作系统的主要语言之一,因为它提供了底层访问硬件和内存的能力,并且具有高性能和效率。 2. 编写嵌入式系统:C语言被广泛应用于嵌入式系统的开发,如智能手机、汽车电子设备和家电等。它可以直接访问硬件,并且具有高效的内存管理能力。 3. 创建网络应用程序:C语言具有处理底层网络协议和数据包的功能,可以用于编写网络应用程序,如服务器、网络安全工具和通信软件。 4. 实现算法和数据结构:C语言是编写高效算法和数据结构的理想语言。它提供了丰富的数据类型和强大的指针操作,使得实现复杂的数据结构和算法变得容易。 5. 开发游戏和图形应用程序:C语言可以用于开发游戏和图形应用程序。它可以直接访问计算机的图形硬件,提供了快速和灵活的图形处理能力。 6. 编写设备驱动程序:C语言可以用于编写设备驱动程序,使操作系统能够与硬件设备进行通信和控制。 总结来说,C语言是一种功能强大且灵活的编程语言,适用于各种领域的开发。它具有高性能、底层访问能力和丰富的库支持,因此被广泛应用于系统级编程、嵌入式系统和网络应用程序的开发。 ### 回答3: 编程语言C是一种高级语言,它是由贝尔实验室的Dennis Ritchie于1972年开发的。C语言具有简洁、高效、跨平台的特点,因此广泛应用于系统开发、游戏开发、嵌入式软件开发等领域。 首先,C语言的语法相对简单,易于学习和理解。它由一系列的关键字和语法规则组成,具有清晰的逻辑结构和丰富的表达能力。使用C语言编写程序,可以直观地表达出想要实现的算法和逻辑。 其次,C语言具有高效的运行速度和低的内存占用。C语言的代码经过编译后生成机器语言,可以直接在计算机上运行,不需要解释器。相比于其他高级语言,C语言的执行速度更快,内存消耗更少,可以更好地满足对性能要求较高的应用场景。 此外,C语言具有良好的可移植性。由于C语言是跨平台的,一次编写的C代码可以在多个操作系统和硬件平台上运行。这使得使用C语言编写的软件更加灵活,有利于程序的开发和维护。 最后,C语言还具有丰富的函数库和工具支持。C语言的标准库提供了丰富的函数和工具,可以方便地实现各种功能,如输入输出、字符串操作、数学计算等。此外,还有许多第三方库可以用于更复杂的任务,如图形处理、网络通信等。 总而言之,C语言是一种强大而灵活的编程语言,可以广泛应用于各种领域。无论是对初学者来说,还是对有经验的开发人员来说,掌握C语言都是一种很有价值的技能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值