1. 程式人生 > >printf不定引數

printf不定引數


title: printf不定引數
tags: C ARM
date: 2018-10-21 12:14:58
---

不定引數的傳遞

函式呼叫時引數傳遞是使用堆疊來實現的,引數入棧順序是從右向左,在被呼叫函式 (Callee) 返回後,由呼叫方 (Caller)調整堆疊,由於這種約定,C呼叫約定允許函式的引數的個數是不固定的,這也是C語言的一大特色。因為每個呼叫的地方都需要生成一段清理堆疊的程式碼,所以最後生成的目標檔案較__stdcall、__fastcall呼叫方式要大,因為每一個主調函式在每個呼叫的地方都需要生成一段清理堆疊的程式碼。C呼叫約定在返回前,要作一次堆疊平衡,也就是引數入棧了多少位元組,就要彈出來多少位元組.這樣很安全.

C程式棧底為高地址,棧頂為低地址,ARM棧一般為向下生長,C51為向上生長,但是在引數傳遞的時候,沒有使用這個棧,而是使用固定地址,他形成的也是一個向下生長的棧,具體看51中的引數傳遞

一些規定

  • 不定引數的列表必須在整個函式的引數列表的最後
  • 函式呼叫時引數傳遞是使用堆疊來實現的
  • 在51 和arm中,不論怎麼傳遞引數,都確保是向下生長的棧(51是模擬的一個),第一個引數在最低地址即可,否則如果是向上生長的堆疊,取引數的巨集就需要做改變了
  • 注意傳遞取地址的時候,注意位元組對其
  • printf 中 float 型別的資料預設按照double型別傳遞

使用不定引數

  1. 獲取定參地址,首地址
  2. 對連續地址進行依次取值,移動指標到下一個引數的地址

確定不定引數個數

  • 在型別固定的引數中指明後面有多少個引數以及他們的型別。printf就是採用的這種方法,它的format引數指明後面每個引數的型別。
  • 指定一個結束引數。這種情況一般是不定引數擁有同樣的型別,我們可以指定一個特定的值來表示引數列表結束。

使用巨集來操作不定參地址

簡單的程式碼例項:

void TestPush(const char * format,...)
{

    //format 本身是個指標,指向 const char 
    //format 本身存在sp中,獲取format的地址也就是堆疊所在的地址
    //注意這裡獲取的format的地址,不能直接是format

    char * sp_start=(char* )&format;
    void * pt;

    printf("arg[0]=%s\r\n",format); 

    pt=(int*)(sp_start+sizeof(const char *));
    printf("arg[1]=%d\r\n",*(int*)pt);

    pt=(double*)(pt+sizeof(int));
    printf("arg[2]=%f\r\n",*(double*)pt);

    pt=(int*)(pt+sizeof(double));
    printf("arg[3]=%d\r\n",*(int*)pt);

    //注意這裡指標轉換
    //錯誤:(struct person_stu*)pt->name 先結合後面的pt->name,然後轉換地址
    //這裡的->優先順序高於低於地址轉換

    pt=(struct person_stu*)(pt+sizeof(int));
    printf("arg[4]=struct person\r\n");
    printf("        name=%s,age=%d,weight=%d\r\n",
                            ((struct person_stu*)pt)->name,
                            ((struct person_stu*)pt)->age,
                            ((struct person_stu*)pt)->weight);

}

mark

注意:

tips 之前一直在想如果是結構體一個成員傳遞是不是有對齊的問題。。。後來想起來這東西是值傳遞啊,但是在傳遞比如 char 這個型別的時候,為了保證4位元組對齊,應將其改為int va_arg(int) 而不是va_arg(char)

typedef char *  va_list;
#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )// int 對齊
#define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) ) //查詢第一個引數的值,同時指標指向下一個
#define va_arg(ap,t)    (*(t *)(ap = ap + _INTSIZEOF(t), ap - _INTSIZEOF(t))) //取值,取值
逗號表示式返回後一個表示式的值
表示式1:ap = ap + _INTSIZEOF(t)
表示式2:*(t *)(ap + _INTSIZEOF(t)) //因為第一個表示式加了地址偏移了
#define va_end(ap)      ( ap = (va_list)0 ) 

注意:

由於引數的地址用於va_start巨集,所以引數不能宣告為暫存器變數或陣列型別,陣列的話是地址進去了應該。

51的巨集有所不同,具體去看STDARG.H

#define __STDARG_H__

#ifndef NULL
 #define NULL ((void *) 0)
#endif

#ifndef _VA_LIST_DEFINED
typedef char *va_list;
#define _VA_LIST_DEFINED
#endif

#define va_start(ap,v) ap = (va_list)&v + sizeof(v)
#define va_arg(ap,t) (((t *)ap)++[0])
#define va_end(ap)

#endif

ARM傳遞引數

  • 當少於四個時,按從左到右的順序依次放在r0,r1,r2,r3中;
  • 當多於四個時,前四個放在r0,r1,r2,r3中,剩餘的放在堆疊中,最後一個引數先入棧,第五個引數最後入棧,即從右到左入棧

例子如下:,傳遞引數先在r0,r1,r2,r3,然後放進棧中

    48:            testStack("1",(int)2,(int)3,(int)4,(int)5,(int)6,(int)7,(int)8,(int)9,(int)10,(int)11,(int)12,(int)13); 
0x0800028A 200D      MOVS     r0,#0x0D
0x0800028C 210C      MOVS     r1,#0x0C
0x0800028E 220B      MOVS     r2,#0x0B
0x08000290 230A      MOVS     r3,#0x0A
0x08000292 E9CD3205  STRD     r3,r2,[sp,#0x14]
0x08000296 E9CD1007  STRD     r1,r0,[sp,#0x1C]
0x0800029A 2009      MOVS     r0,#0x09
0x0800029C 2108      MOVS     r1,#0x08
0x0800029E 2207      MOVS     r2,#0x07
0x080002A0 2306      MOVS     r3,#0x06
0x080002A2 E9CD3201  STRD     r3,r2,[sp,#0x04]
0x080002A6 E9CD1003  STRD     r1,r0,[sp,#0x0C]
0x080002AA 2005      MOVS     r0,#0x05
0x080002AC 2304      MOVS     r3,#0x04
0x080002AE 2203      MOVS     r2,#0x03
0x080002B0 2102      MOVS     r1,#0x02
0x080002B2 9000      STR      r0,[sp,#0x00]
0x080002B4 A014      ADR      r0,{pc}+4  ; @0x08000308
0x080002B6 F7FFFFBD  BL.W     testStack (0x08000234)

使用時直接用堆疊,取出r0~~r3的值

    10: void testStack(char * format, ...) 
0x08000232 4770      BX       lr
    11: { 
0x08000234 B40F      PUSH     {r0-r3}
0x08000236 B570      PUSH     {r4-r6,lr}
    12:         char *p = (char * )&format; 
    13:         int i; 
    14:         int b; 
    15:  
    16:          
0x08000238 AC04      ADD      r4,sp,#0x10

.........................

當引數個數較少時, 可能也就單純的賦值到r0~~r3

#include "stdio.h"

int  a1(int b)
{
  return (b)+1;
}


long  a[10];
void SystemInit(){;}
void testStack(int format, ...)
{
  char *p = (char * )&format;
  int i;
  int b;
  a[0]=format;
  a[1]=(long)&format;
  p = p + sizeof(int);

  b=(int)p;
  a1(b);
  
  i = *((int *)p);
  a[2]=*p;
  a[3]=(long)p;

a[4]=(long)p;         
}

int main()
{
  while(1)
  {
      testStack((int)1);
      testStack((int)1,(int)2);
      testStack((int)1,(int)2,(int)3);
      testStack((int)1,(int)2,(int)3,(int)4);
      testStack((int)1,0x11e23344,(int)0x11223f43,(int)0x11d23342,(int)0xa1223341);
    testStack((int)1,(int)2,(int)3,(int)4,(int)5,(int)6,(int)7,(int)8,(int)9,(int)10,(int)11,(int)12,(int)13);
  }
}

實驗結果,可以看出是sp裡面的取值地址

a0=0x00000001
a1=0x200003F0
a2=0x00000002  其中有=44,因為第二個引數有不一樣的
a3=0x200003F4

51中引數傳遞

testStack("555555555",(int)2,(int)3,(int)4,(int)5,(int)6,(int)7,(int)8,(int)9,(int)10);

C:0x0F66    7BFF     MOV      R3,#0xFF
C:0x0F68    7A16     MOV      R2,#0x16
C:0x0F6A    7996     MOV      R1,#0x96
C:0x0F6C    752800   MOV      0x28,#0x00
C:0x0F6F    752902   MOV      0x29,#0x02
C:0x0F72    752A00   MOV      0x2A,#0x00
C:0x0F75    752B03   MOV      0x2B,#0x03
C:0x0F78    752C00   MOV      0x2C,#0x00
C:0x0F7B    752D04   MOV      0x2D,#0x04
C:0x0F7E    752E00   MOV      0x2E,#0x00
C:0x0F81    752F05   MOV      0x2F,#0x05
C:0x0F84    753000   MOV      0x30,#0x00
C:0x0F87    753106   MOV      0x31,#0x06
C:0x0F8A    753200   MOV      0x32,#0x00
C:0x0F8D    753307   MOV      0x33,#0x07
C:0x0F90    753400   MOV      0x34,#0x00
C:0x0F93    753508   MOV      0x35,#0x08
C:0x0F96    753600   MOV      0x36,#0x00
C:0x0F99    753709   MOV      0x37,#0x09
C:0x0F9C    753800   MOV      0x38,#0x00
C:0x0F9F    75390A   MOV      0x39,#0x0A
C:0x0FA2    121515   LCALL    testStack(C:1515)



void testStack(char * format, ...)
{
  char *p = (char * )&format;
  int i;
  int b;
  printf("arg1 : %s\n",format);
  printf("&arg1 : %p\n",&format);

  /*指標對連續空間操作時: 1) 取值  2)移動指標*/  
  p = p + sizeof(char *);
  b=(int)p;
  a1(b);
  i = *((int *)p);
  printf("arg2 : %d\n",i);
  printf("&arg2 : %p\n",p);       
}

列印
arg1 : 1
&arg1 : i:0025
arg2 : 2
&arg2 : i:0028

函式呼叫的情況如下,可以看出先 MOV R1,R2,R3 -->0x27,0x26,0x25,之前R1,R2,R3正好是第一個引數的值,而之前其使用固定地址傳值的地址正好是0x28-->0x39,其實後面再使用這個函式的時候,0x28總保持不變的,0x39根據可變引數個數來變,這就好辦了,類似於知道棧頂地址為0x28,棧底我們可以根據不同方法來確認了(向下生長的棧)。

     9: void testStack(char * format, ...) 
C:0x0418    8B25     MOV      0x25,R3
C:0x041A    8A26     MOV      0x26,R2
C:0x041C    8927     MOV      0x27,R1
    10: { 
    11:         char *p = (char * )&format; 
    12:         int i; 
    13:         int b; 
    14:  
    15:          
C:0x041E    753700   MOV      0x37,#0x00
C:0x0421    753800   MOV      0x38,#0x00
C:0x0424    753925   MOV      0x39,#0x25
    16:         printf("arg1 : %s\n",format); 
C:0x0427    8B3D     MOV      0x3D,R3
C:0x0429    8A3E     MOV      0x3E,R2
C:0x042B    893F     MOV      0x3F,R1
C:0x042D    7BFF     MOV      R3,#0xFF
C:0x042F    7A04     MOV      R2,#0x04
C:0x0431    79E3     MOV      R1,#0xE3
C:0x0433    120065   LCALL    PRINTF(C:0065)

    17:         printf("&arg1 : %p\n",&format); 
    18:          
    19:          
    20:         /*指標對連續空間操作時: 1) 取值  2)移動指標*/   
C:0x0436    753D00   MOV      0x3D,#0x00
C:0x0439    753E00   MOV      0x3E,#0x00
C:0x043C    753F25   MOV      0x3F,#0x25
C:0x043F    7BFF     MOV      R3,#0xFF
C:0x0441    7A04     MOV      R2,#0x04
C:0x0443    79EE     MOV      R1,#0xEE
C:0x0445    120065   LCALL    PRINTF(C:0065)
    21:         p = p + sizeof(char *); 
    22:  
C:0x0448    7403     MOV      A,#0x03
C:0x044A    2539     ADD      A,0x39
C:0x044C    F539     MOV      0x39,A
C:0x044E    E4       CLR      A
C:0x044F    3538     ADDC     A,0x38
C:0x0451    F538     MOV      0x38,A
    23:         b=(int)p; 
C:0x0453    FE       MOV      R6,A
C:0x0454    AF39     MOV      R7,0x39
    24:         a1(b); 
    
        25:          
C:0x0456    12056F   LCALL    a1(C:056F)
    26:         i = *((int *)p); 
C:0x0459    AB37     MOV      R3,0x37
C:0x045B    AA38     MOV      R2,0x38
C:0x045D    A939     MOV      R1,0x39
C:0x045F    12035F   LCALL    C?ILDPTR(C:035F)
    27:         printf("arg2 : %d\n",i); 
C:0x0462    7BFF     MOV      R3,#0xFF
C:0x0464    7A04     MOV      R2,#0x04
C:0x0466    79FA     MOV      R1,#0xFA
C:0x0468    85F03D   MOV      0x3D,B(0xF0)
C:0x046B    F53E     MOV      0x3E,A
C:0x046D    120065   LCALL    PRINTF(C:0065)
    28:         printf("&arg2 : %p\n",p);        
    29:                  
    30:  
    31: } 

列印的結果如下,符合預期.R1,R2,R3 儲存第一個引數的地址,後面的引數使用固定地址,最左邊的引數所在地址越低,這就導致了和 使用向下生長的棧,且引數從右向左是一致的效果,然後在函式呼叫的時候講R1,R2,R3的值再賦值給更低的固定地址。

arg1 : 1
&arg1 : i:0025
arg2 : 2
&arg2 : i:0028

main.c 如下,使用軟體模擬即可

#include "stdio.h"
#include <reg51.h>

int  a1(int b)
{
  return (b)+1;
}

void testStack(char * format, ...)
{
  char *p = (char * )&format;
  int i;
  int b;
  printf("arg1 : %s\n",format);
  printf("&arg1 : %p\n",&format);
  /*指標對連續空間操作時: 1) 取值  2)移動指標*/  
  p = p + sizeof(char *);

  b=(int)p;
  a1(b);
  
  i = *((int *)p);
  printf("arg2 : %d\n",i);
  printf("&arg2 : %p\n",p);               
}
void t()
{
  SCON= 0x50;/*SCON:工作模式1,8-bit UART,允許接收*/
  TMOD |= 0x20;/*TMOD:定時器T1,工作模式2, 8位自動過載方式*/
  TH1= 0xf3;/*當波特率為2400時,定時器初值*/
  TR1= 1;/*定時器T1開始執行*/
  TI= 1;/*允許傳送資料*/
}
#include<intrins.h>       // 聲明瞭void _nop_(void);
void main()
{
  _nop_();                         // 產生一條NOP指令
  _nop_();                         // 產生一條NOP指令
  _nop_();                         // 產生一條NOP指令
  _nop_();                         // 產生一條NOP指令
  _nop_();                         // 產生一條NOP指令
  _nop_();                         // 產生一條NOP指令
  _nop_();                         // 產生一條NOP指令
  t();
  {
      char * str="kakakq=%d,%d,%d,%d,%d,%d\r\n";
      printf(str,(int)1,(int)2,(int)3,(int)4,(int)5,(int)6);  
  }
  while(1)
  {
     testStack("1",(int)2,(int)3,(int)4,(int)5);
      _nop_();                         // 產生一條NOP指令
  }
}

mark

printf 進位制轉換關鍵演算法

unsigned char hex_tab[]={'0','1','2','3','4','5','6','7',\
                         '8','9','a','b','c','d','e','f'};
    do{
        *--s = hex_tab[m%base];
        count++;
    }while ((m /= base) != 0);
// n 數字
// base 進位制
// lead 前導字元
// maxwith 字元寬度
static int out_num(long n, int base,char lead,int maxwidth) 
{
    unsigned long m=0;
    char buf[MAX_NUMBER_BYTES], *s = buf + sizeof(buf);
    int count=0,i=0;
            

    *--s = '\0';
    
    if (n < 0){
        m = -n;
    }
    else{
        m = n;
    }
    
    do{
        *--s = hex_tab[m%base];
        count++;
    }while ((m /= base) != 0);
    
    if( maxwidth && count < maxwidth){
        for (i=maxwidth - count; i; i--)    
            *--s = lead;
}

    if (n < 0)
        *--s = '-';
    
    return outs(s);
}

除法

剛開始的Makefile如下,會報錯提示除法找不到

all:
    arm-linux-gcc -c -o start.o start.S
    arm-linux-gcc -c -o led.o led.c
    arm-linux-gcc -c -o uart.o uart.c
    arm-linux-gcc -c -o my_printf.o my_printf.c
    arm-linux-gcc -c -o main.o main.c

    arm-linux-ld -Ttext 0 start.o led.o uart.o   my_printf.o main.o -o uart.elf
    
    arm-linux-objcopy -O binary -S uart.elf uart.bin
    arm-linux-objdump -D uart.elf > uart.dis
clean:
    rm *.bin *.o *.elf *.dis

網上搜索說加入libgcc.a我們搜尋下gcc的路徑,然後搜尋這個檔案並加入,發現還是失敗了

which gcc 

$ find /opt/gcc-3.4.5-glibc-2.3.6/ -name libgcc.a
$ /opt/gcc-3.4.5-glibc-2.3.6/lib/gcc/arm-linux/3.4.5/libgcc.a

l連結的時候加入,失敗

arm-linux-ld -Ttext 0 start.o led.o uart.o  /opt/gcc-3.4.5-glibc-2.3.6/lib/gcc/arm-linux/3.4.5/libgcc.a  my_printf.o main.o -o uart.elf

視訊上說使用,然後連結.這樣成功了,但是不知道怎麼弄的

arm-linux-gcc -c -o lib1funcs.o lib1funcs.S

空間大小

但是這樣編譯出來的程式碼有30多k

[email protected]:~/stu/004-printf/3th$ ls -l *.bin
-rwxrwxr-x 1 book book 36328 10月 21 16:23 uart.bin

檢視下反彙編的section,發現程式碼段在0起始,但是資料段在00008dd8,這個值差不多就是30多k了

tips: vim使用/ 啟用搜索,確認回車後,使用 n向前,N向後搜尋

 Disassembly of section .data:
 935
 936 00008dd8 <__data_start>:
 937     8dd8:   33323130    teqcc   r2, #12 ; 0xc
 938     8ddc:   37363534    undefined
 939     8de0:   62613938    rsbvs   r3, r1, #917504 ; 0xe0000
 940     8de4:   66656463    strvsbt r6, [r5], -r3, ror #8
 941 Disassembly of section .comment:

重定位資料段的位置,檢視下這個段上面的只讀資料段的尾巴,視訊的程式碼在e10)只需要稍後一點定義即可,加入選項-Tdata 0xe80

[email protected]:~/stu/004-printf/3th$ ls -l *.bin
-rwxrwxr-x 1 book book 3728 10月 21 16:31 uart.bin

最終Makefile如下

all:
    arm-linux-gcc -c -o start.o start.S
    arm-linux-gcc -c -o led.o led.c
    arm-linux-gcc -c -o uart.o uart.c
    
    arm-linux-gcc -c -o lib1funcs.o lib1funcs.S
    arm-linux-gcc -c -o my_printf.o my_printf.c
    arm-linux-gcc -c -o main.o main.c

    arm-linux-ld -Ttext 0 -Tdata 0xe80 start.o led.o uart.o  lib1funcs.o my_printf.o main.o -o uart.elf
    
    arm-linux-objcopy -O binary -S uart.elf uart.bin
    arm-linux-objdump -D uart.elf > uart.dis
    
    rm *.o 
clean:
    rm *.bin *.o *.elf *.dis