博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
uboot学习之BL3的流程
阅读量:6332 次
发布时间:2019-06-22

本文共 21138 字,大约阅读时间需要 70 分钟。

BL2的最后通过汇编调用了board_init_r函数,此时进入BL3的阶段,此时的主要工作:

这一阶段涉及的文件及任务如下

arch/arm/lib/board.c

           1. board_init_r()是进入定制板目录的入口
common/main.c
           2. main_loop()中关闭中断,执行命令以及加载引导内核

下面分析一下board_init_r函数:

/* ************************************************************************ * * This is the next part if the initialization sequence: we are now * running from RAM and have a "normal" C environment, i. e. global * data can be written, BSS has been cleared, the stack size in not * that critical any more, etc. * ************************************************************************ */void board_init_r(gd_t *id, ulong dest_addr){    ...    bd_t *bd;    ...    gd = id;    bd = gd->bd;    gd->flags |= GD_FLG_RELOC;    /* tell others: relocation done */    monitor_flash_len = _end_ofs;    ...    debug("monitor flash len: %08lX\n", monitor_flash_len);    board_init();    /* Setup chipselects */

上述代码的作用是对gd和bd进行赋值,其中monitor_flash_len为整个U-Boot的长度。

malloc_start = dest_addr - TOTAL_MALLOC_LEN - sizeof(struct spare_boot_head_t);.../* The Malloc area is immediately below the monitor copy in DRAM */    mem_malloc_init (malloc_start, TOTAL_MALLOC_LEN);

对SDRAM中的malloc空间进行清零初始化。

#if !defined(CONFIG_SYS_NO_FLASH)    puts("Flash: ");    flash_size = flash_init();    if (flash_size > 0) {# ifdef CONFIG_SYS_FLASH_CHECKSUM        print_size(flash_size, "");        /*         * Compute and print flash CRC if flashchecksum is set to 'y'         *         * NOTE: Maybe we should add some WATCHDOG_RESET()? XXX         */        s = getenv("flashchecksum");        if (s && (*s == 'y')) {            printf("  CRC: %08X", crc32(0,                (const unsigned char *) CONFIG_SYS_FLASH_BASE,                flash_size));        }        putc('\n');# else    /* !CONFIG_SYS_FLASH_CHECKSUM */        print_size(flash_size, "\n");# endif /* CONFIG_SYS_FLASH_CHECKSUM */    } else {        puts(failed);        hang();    }#endif

上述代码的作用是计算FLASH的大小,并把它通过串口显示在控制台上。由于没有定义CONFIG_SYS_FLASH_CHECKSUM,所以没有执行CRC的校验和。其中flash_init函数是在drivers/mtd目录下的cfi_flash.c文件内(因为include/configs/smdk2410.h中定义了CONFIG_FLASH_CFI_DRIVER)。

/* set up exceptions */    interrupt_init();    /* enable exceptions */    enable_interrupts();

interrupt_init函数是建立IRQ中断堆栈,enable_interrupts函数是使能IRQ中断,它们都是在arch/arm/lib目录下的interrupts.c文件中定义的。

.../* initialize environment */    env_relocate();

初始化环境变量,由于gd->env_valid等于0,所以在这里设置的是缺省环境变量。env_relocate函数是在common目录下的env_common.c文件中定义的。

#if defined(CONFIG_CMD_PCI) || defined(CONFIG_PCI)    arm_pci_init();#endif

初始化PCI。

/* IP Address */    gd->bd->bi_ip_addr = getenv_IPaddr("ipaddr");    stdio_init();    /* get the devices list going. */    jumptable_init();#if defined(CONFIG_API)    /* Initialize API */    api_init();#endif    console_init_r();    /* fully init console as a device */

上面代码的作用分别对应:

设置IP地址。

初始化各类外设,如IIC、LCD、键盘、USB等,当然只有在定义了这些外设的前提下,才对这些外设进行初始化。该函数是在common目录下的stdio.c文件中定义的。

初始化跳转表gd->jt,该跳转表是一个函数指针数组,它定义了U-Boot中基本的常用函数库。该函数是在common目录下的exports.c文件中定义的。

初始化API。

初始化控制台,即标准输入、标准输出和标准错误,在这里都是串口。该函数是在common目录下的console.c文件中定义的。

/* Initialize from environment */    s = getenv("loadaddr");    if (s != NULL)        load_addr = simple_strtoul(s, NULL, 16);

从环境变量中获取loadaddr参数,得到需要加载的地址。

#if defined(CONFIG_CMD_NET)    s = getenv("bootfile");    if (s != NULL)        copy_filename(BootFile, s, sizeof(BootFile));#endif

从环境变量中获取bootfile参数,得到通过TFTP加载的镜像文件名。

#if defined(CONFIG_CMD_NET)#if defined(CONFIG_NET_MULTI)    puts("Net:   ");#endif    eth_initialize(gd->bd);#if defined(CONFIG_RESET_PHY_R)    debug("Reset Ethernet PHY\n");    reset_phy();#endif#endif

初始化以太网,其中eth_initialize函数是在net目录下的eth.c文件中定义的。

...   /* main_loop() can return to retry autoboot, if so just run it again. */    for (;;)    {        main_loop();    }    hang();    /* NOTREACHED - no way out of command loop except booting */}

board_init_r函数的最后就是执行一个死循环,调用main_loop函数。该函数是在common目录下的main.c文件内定义的。

总结:

3.接下来分析main_loop函数:

void main_loop (void){#ifndef CONFIG_SYS_HUSH_PARSER    static char lastcommand[CONFIG_SYS_CBSIZE] = { 0, };    int len;    int rc = 1;    int flag;#endif

声明一些hush参数。关于hush后面会讲到。

#if defined(CONFIG_BOOTDELAY) && (CONFIG_BOOTDELAY >= 0)    char *s;    int bootdelay;#endif

声明启动延时需要的参数。

#ifdef CONFIG_SYS_HUSH_PARSER    u_boot_hush_start ();#endif

初始化hush功能。稍后再说。

1 #if defined(CONFIG_BOOTDELAY) && (CONFIG_BOOTDELAY >= 0) 2     s = getenv ("bootdelay"); 3     bootdelay = s ? (int)simple_strtol(s, NULL, 10) : CONFIG_BOOTDELAY; 4  5     debug ("### main_loop entered: bootdelay=%d\n\n", bootdelay); 6  7 # ifdef CONFIG_BOOT_RETRY_TIME 8     init_cmd_timeout (); 9 # endif    /* CONFIG_BOOT_RETRY_TIME */10 11 #ifdef CONFIG_POST12     if (gd->flags & GD_FLG_POSTFAIL) {13         s = getenv("failbootcmd");14     }15     else16 #endif /* CONFIG_POST */17 #ifdef CONFIG_BOOTCOUNT_LIMIT18     if (bootlimit && (bootcount > bootlimit)) {19         printf ("Warning: Bootlimit (%u) exceeded. Using altbootcmd.\n",20                 (unsigned)bootlimit);21         s = getenv ("altbootcmd");22     }23     else24 #endif /* CONFIG_BOOTCOUNT_LIMIT */25         s = getenv ("bootcmd");26 27     debug ("### main_loop: bootcmd=\"%s\"\n", s ? s : "
");28 29 if (bootdelay >= 0 && s && !abortboot (bootdelay)) {30 # ifdef CONFIG_AUTOBOOT_KEYED31 int prev = disable_ctrlc(1); /* disable Control C checking */32 # endif33 34 # ifndef CONFIG_SYS_HUSH_PARSER35 run_command (s, 0);36 # else37 parse_string_outer(s, FLAG_PARSE_SEMICOLON |38 FLAG_EXIT_FROM_LOOP);39 # endif40 41 # ifdef CONFIG_AUTOBOOT_KEYED42 disable_ctrlc(prev); /* restore Control C checking */43 # endif44 }45 46 # ifdef CONFIG_MENUKEY47 if (menukey == CONFIG_MENUKEY) {48 s = getenv("menucmd");49 if (s) {50 # ifndef CONFIG_SYS_HUSH_PARSER51 run_command(s, 0);52 # else53 parse_string_outer(s, FLAG_PARSE_SEMICOLON |54 FLAG_EXIT_FROM_LOOP);55 # endif56 }57 }58 #endif /* CONFIG_MENUKEY */59 #endif /* CONFIG_BOOTDELAY */

第2行和第3行的含义是从环境变量中获取bootdelay参数,得到自动启动缺省镜像文件的延时(单位是秒)。

其中bootdelay的作用是:uboot正常启动后,会调用main_loop(void)函数,进入main_loop()之后,如果在规定的时间(CONFIG_BOOTDELAY)内,没有检查到任何按键事件的发生,就会去加载OS,并启动系统。

第8行的含义是初始化命令行超时机制。

第25行的含义是从环境变量中获取bootcmd参数,得到在启动延时过程中自动执行的命令。当我们得到了bootcmd参数,bootdelay参数也是大于等于0,并且在启动延时过程中没有按下任意键时,执行第37行的parse_string_outer函数,该函数的作用是解释bootcmd参数

并执行,它是在common目录下的hush.c文件内定义的。

1     /* 2      * Main Loop for Monitor Command Processing 3      */ 4 #ifdef CONFIG_SYS_HUSH_PARSER 5     parse_file_outer(); 6     /* This point is never reached */ 7     for (;;); 8 #else 9     for (;;) {10 #ifdef CONFIG_BOOT_RETRY_TIME11         if (rc >= 0) {12             /* Saw enough of a valid command to13              * restart the timeout.14              */15             reset_cmd_timeout();16         }17 #endif18         len = readline (CONFIG_SYS_PROMPT);19 20         flag = 0;    /* assume no special flags for now */21         if (len > 0)22             strcpy (lastcommand, console_buffer);23         else if (len == 0)24             flag |= CMD_FLAG_REPEAT;25 #ifdef CONFIG_BOOT_RETRY_TIME26         else if (len == -2) {27             /* -2 means timed out, retry autoboot28              */29             puts ("\nTimed out waiting for command\n");30 # ifdef CONFIG_RESET_TO_RETRY31             /* Reinit board to run initialization code again */32             do_reset (NULL, 0, 0, NULL);33 # else34             return;        /* retry autoboot */35 # endif36         }37 #endif38 39         if (len == -1)40             puts ("
\n");41 else42 rc = run_command (lastcommand, flag);43 44 if (rc <= 0) {45 /* invalid command or not repeatable, forget it */46 lastcommand[0] = 0;47 }48 }49 #endif /*CONFIG_SYS_HUSH_PARSER*/50 }

由于在include/configs/smdk2410.h文件中定义了CONFIG_SYS_HUSH_PARSER,所以上面的代码仅仅执行的是第5行至第7行的内容。第5行的parse_file_outer函数是在common目录下的hush.c文件中定义的,它的含义是依次读取命令序列中的命令并执行之,其中在该

函数还调用了parse_stream_outer函数,这个函数体内有一个do-while循环,只有发生语法错误的时候才会跳出该循环,因此一般情况下永远也不会执行上面代码中的第7行内容,而是始终在那个do-while循环体内。

上面说到过如果在CONFIG_BOOTDELAY时间内,用户按下键盘上的任意一个按键,uboot就会进入与用户交互的状态。如果用户在配置文件中定义了CONFIG_SYS_HUSH_PARSER,就会通过parse_file_outer(),去接收并解析用户命令,否则进入一个for(;;)循环,

通过 readline (CONFIG_SYS_PROMPT)接收用户命令,然后调用run_command(cmd,flag)去解析并执行命令。

当在配置文件中定义了CONFIG_SYS_HUSH_PARSER,main_loop会调用parse_file_outer(),进入hush中。其中parse_file_outer()在u-boot\common\hush.c下。

#ifndef __U_BOOT__static int parse_file_outer(FILE *f)#elseint parse_file_outer(void)#endif{    int rcode;    struct in_str input;#ifndef __U_BOOT__    setup_file_in_str(&input, f);#else    setup_file_in_str(&input);#endif    rcode = parse_stream_outer(&input, FLAG_PARSE_SEMICOLON);    return rcode;}
/* most recursion does not come through here, the exeception is * from builtin_source() */int parse_stream_outer(struct in_str *inp, int flag){    struct p_context ctx;    o_string temp=NULL_O_STRING;    int rcode;#ifdef __U_BOOT__    int code = 0;#endif    do {        ctx.type = flag;        initialize_context(&ctx);        update_ifs_map();        if (!(flag & FLAG_PARSE_SEMICOLON) || (flag & FLAG_REPARSING)) mapset((uchar *)";$&|", 0);        inp->promptmode=1;        rcode = parse_stream(&temp, &ctx, inp, '\n');#ifdef __U_BOOT__        if (rcode == 1) flag_repeat = 0;#endif        if (rcode != 1 && ctx.old_flag != 0) {            syntax();#ifdef __U_BOOT__            flag_repeat = 0;#endif        }        if (rcode != 1 && ctx.old_flag == 0) {            done_word(&temp, &ctx);            done_pipe(&ctx,PIPE_SEQ);#ifndef __U_BOOT__            run_list(ctx.list_head);#else            code = run_list(ctx.list_head);            if (code == -2) {    /* exit */                b_free(&temp);                code = 0;                /* XXX hackish way to not allow exit from main loop */                if (inp->peek == file_peek) {                    printf("exit not allowed from main input shell.\n");                    continue;                }                break;            }            if (code == -1)                flag_repeat = 0;#endif        } else {            if (ctx.old_flag != 0) {                free(ctx.stack);                b_reset(&temp);            }#ifdef __U_BOOT__            if (inp->__promptme == 0) printf("
\n"); inp->__promptme = 1;#endif temp.nonnull = 0; temp.quote = 0; inp->p = NULL; free_pipe_list(ctx.list_head,0); } b_free(&temp); } while (rcode != -1 && !(flag & FLAG_EXIT_FROM_LOOP)); /* loop on syntax errors, return on EOF */#ifndef __U_BOOT__ return 0;#else return (code != 0) ? 1 : 0;#endif /* __U_BOOT__ */}

hush是uboot中命令接收和解析的工具,与uboot原始的命令解析方法相比,该工具更加智能。

这里会在run_list中调用到hush中的run_pipe_real(struct pipe *pi),在该函数中经过一些列解析,最终会调用到对应的命令执行函数。

/* run_pipe_real() starts all the jobs, but doesn't wait for anything * to finish.  See checkjobs(). * * return code is normally -1, when the caller has to wait for children * to finish to determine the exit status of the pipe.  If the pipe * is a simple builtin command, however, the action is done by the * time run_pipe_real returns, and the exit code is provided as the * return value. * * The input of the pipe is always stdin, the output is always * stdout.  The outpipe[] mechanism in BusyBox-0.48 lash is bogus, * because it tries to avoid running the command substitution in * subshell, when that is in fact necessary.  The subshell process * now has its stdout directed to the input of the appropriate pipe, * so this routine is noticeably simpler. */static int run_pipe_real(struct pipe *pi){    int i;#ifndef __U_BOOT__    int nextin, nextout;    int pipefds[2];                /* pipefds[0] is for reading */    struct child_prog *child;    struct built_in_command *x;    char *p;# if __GNUC__    /* Avoid longjmp clobbering */    (void) &i;    (void) &nextin;    (void) &nextout;    (void) &child;# endif#else    int nextin;    int flag = do_repeat ? CMD_FLAG_REPEAT : 0;    struct child_prog *child;    cmd_tbl_t *cmdtp;    char *p;# if __GNUC__    /* Avoid longjmp clobbering */    (void) &i;    (void) &nextin;    (void) &child;# endif#endif    /* __U_BOOT__ */    nextin = 0;#ifndef __U_BOOT__    pi->pgrp = -1;#endif    /* Check if this is a simple builtin (not part of a pipe).     * Builtins within pipes have to fork anyway, and are handled in     * pseudo_exec.  "echo foo | read bar" doesn't work on bash, either.     */    if (pi->num_progs == 1) child = & (pi->progs[0]);#ifndef __U_BOOT__    if (pi->num_progs == 1 && child->group && child->subshell == 0) {        int squirrel[] = {-1, -1, -1};        int rcode;        debug_printf("non-subshell grouping\n");        setup_redirects(child, squirrel);        /* XXX could we merge code with following builtin case,         * by creating a pseudo builtin that calls run_list_real? */        rcode = run_list_real(child->group);        restore_redirects(squirrel);#else        if (pi->num_progs == 1 && child->group) {        int rcode;        debug_printf("non-subshell grouping\n");        rcode = run_list_real(child->group);#endif        return rcode;    } else if (pi->num_progs == 1 && pi->progs[0].argv != NULL) {        for (i=0; is_assignment(child->argv[i]); i++) { /* nothing */ }        if (i!=0 && child->argv[i]==NULL) {            /* assignments, but no command: set the local environment */            for (i=0; child->argv[i]!=NULL; i++) {                /* Ok, this case is tricky.  We have to decide if this is a                 * local variable, or an already exported variable.  If it is                 * already exported, we have to export the new value.  If it is                 * not exported, we need only set this as a local variable.                 * This junk is all to decide whether or not to export this                 * variable. */                int export_me=0;                char *name, *value;                name = xstrdup(child->argv[i]);                debug_printf("Local environment set: %s\n", name);                value = strchr(name, '=');                if (value)                    *value=0;#ifndef __U_BOOT__                if ( get_local_var(name)) {                    export_me=1;                }#endif                free(name);                p = insert_var_value(child->argv[i]);                set_local_var(p, export_me);                if (p != child->argv[i]) free(p);            }            return EXIT_SUCCESS;   /* don't worry about errors in set_local_var() yet */        }        for (i = 0; is_assignment(child->argv[i]); i++) {            p = insert_var_value(child->argv[i]);#ifndef __U_BOOT__            putenv(strdup(p));#else            set_local_var(p, 0);#endif            if (p != child->argv[i]) {                child->sp--;                free(p);            }        }        if (child->sp) {            char * str = NULL;            str = make_string((child->argv + i));            parse_string_outer(str, FLAG_EXIT_FROM_LOOP | FLAG_REPARSING);            free(str);            return last_return_code;        }#ifndef __U_BOOT__        for (x = bltins; x->cmd; x++) {            if (strcmp(child->argv[i], x->cmd) == 0 ) {                int squirrel[] = {-1, -1, -1};                int rcode;                if (x->function == builtin_exec && child->argv[i+1]==NULL) {                    debug_printf("magic exec\n");                    setup_redirects(child,NULL);                    return EXIT_SUCCESS;                }                debug_printf("builtin inline %s\n", child->argv[0]);                /* XXX setup_redirects acts on file descriptors, not FILEs.                 * This is perfect for work that comes after exec().                 * Is it really safe for inline use?  Experimentally,                 * things seem to work with glibc. */                setup_redirects(child, squirrel);#else            /* check ";", because ,example , argv consist from             * "help;flinfo" must not execute             */            if (strchr(child->argv[i], ';')) {                printf ("Unknown command '%s' - try 'help' or use 'run' command\n",                    child->argv[i]);                return -1;            }            /* Look up command in command table */            if ((cmdtp = find_cmd(child->argv[i])) == NULL) {                printf ("Unknown command '%s' - try 'help'\n", child->argv[i]);                return -1;    /* give up after bad command */            } else {                int rcode;#if defined(CONFIG_CMD_BOOTD)                /* avoid "bootd" recursion */                if (cmdtp->cmd == do_bootd) {                    if (flag & CMD_FLAG_BOOTD) {                        printf ("'bootd' recursion detected\n");                        return -1;                    }                else                    flag |= CMD_FLAG_BOOTD;                }#endif                /* found - check max args */                if ((child->argc - i) > cmdtp->maxargs)                    return cmd_usage(cmdtp);#endif                child->argv+=i;  /* XXX horrible hack */#ifndef __U_BOOT__                rcode = x->function(child);#else                /* OK - call function to do the command */                rcode = (cmdtp->cmd)(cmdtp, flag,child->argc-i,&child->argv[i]);                if ( !cmdtp->repeatable )                    flag_repeat = 0;#endif                child->argv-=i;  /* XXX restore hack so free() can work right */#ifndef __U_BOOT__                restore_redirects(squirrel);#endif                return rcode;            }        }#ifndef __U_BOOT__    }    for (i = 0; i < pi->num_progs; i++) {        child = & (pi->progs[i]);        /* pipes are inserted between pairs of commands */        if ((i + 1) < pi->num_progs) {            if (pipe(pipefds)<0) perror_msg_and_die("pipe");            nextout = pipefds[1];        } else {            nextout=1;            pipefds[0] = -1;        }        /* XXX test for failed fork()? */        if (!(child->pid = fork())) {            /* Set the handling for job control signals back to the default.  */            signal(SIGINT, SIG_DFL);            signal(SIGQUIT, SIG_DFL);            signal(SIGTERM, SIG_DFL);            signal(SIGTSTP, SIG_DFL);            signal(SIGTTIN, SIG_DFL);            signal(SIGTTOU, SIG_DFL);            signal(SIGCHLD, SIG_DFL);            close_all();            if (nextin != 0) {                dup2(nextin, 0);                close(nextin);            }            if (nextout != 1) {                dup2(nextout, 1);                close(nextout);            }            if (pipefds[0]!=-1) {                close(pipefds[0]);  /* opposite end of our output pipe */            }            /* Like bash, explicit redirects override pipes,             * and the pipe fd is available for dup'ing. */            setup_redirects(child,NULL);            if (interactive && pi->followup!=PIPE_BG) {                /* If we (the child) win the race, put ourselves in the process                 * group whose leader is the first process in this pipe. */                if (pi->pgrp < 0) {                    pi->pgrp = getpid();                }                if (setpgid(0, pi->pgrp) == 0) {                    tcsetpgrp(2, pi->pgrp);                }            }            pseudo_exec(child);        }        /* put our child in the process group whose leader is the           first process in this pipe */        if (pi->pgrp < 0) {            pi->pgrp = child->pid;        }        /* Don't check for errors.  The child may be dead already,         * in which case setpgid returns error code EACCES. */        setpgid(child->pid, pi->pgrp);        if (nextin != 0)            close(nextin);        if (nextout != 1)            close(nextout);        /* If there isn't another process, nextin is garbage           but it doesn't matter */        nextin = pipefds[0];    }#endif    return -1;}
View Code

上面关键是这段:

/* OK - call function to do the command */                rcode = (cmdtp->cmd)(cmdtp, flag,child->argc-i,&child->argv[i]);                if ( !cmdtp->repeatable )                    flag_repeat = 0;

通过这段代码调用对应的命令执行函数。cmd_tbl_t的结构如下:

struct cmd_tbl_s {    char        *name;        /* Command Name            */    int        maxargs;    /* maximum number of arguments    */    int        repeatable;    /* autorepeat allowed?        */                    /* Implementation function    */    int        (*cmd)(struct cmd_tbl_s *, int, int, char * const []);    char        *usage;        /* Usage message    (short)    */#ifdef    CONFIG_SYS_LONGHELP    char        *help;        /* Help  message    (long)    */#endif#ifdef CONFIG_AUTO_COMPLETE    /* do auto completion on the arguments */    int        (*complete)(int argc, char * const argv[], char last_char, int maxv, char *cmdv[]);#endif};typedef struct cmd_tbl_s    cmd_tbl_t;

对于uboot支持的每一个命令,是通过U_BOOT_CMD宏定义的,他定义了该命令对应的名称name,支持的最大参数rep,重复次数,实现函数cmd,以及输入help命令时,显示的帮助信息usage。

在执行函数cmd中,第一个参数对应该命令结构本身的指针,第二个参数对应flag标记,第三个参数对应参数数目,第四个参数是指针数组,里面存储的是对应参数的指针。

 

转载于:https://www.cnblogs.com/yeqluofwupheng/p/7372849.html

你可能感兴趣的文章
Hive学习之路 (七)Hive的DDL操作
查看>>
[转]mysql使用关键字作为列名的处理方式
查看>>
awesome go library 库,推荐使用的golang库
查看>>
树形展示形式的论坛
查看>>
jdbcTemplate 调用存储过程。 入参 array 返回 cursor
查看>>
C++中的stack类、QT中的QStack类
查看>>
Linux常用基本命令[cp]
查看>>
CSS 相对|绝对(relative/absolute)定位系列(一)
查看>>
关于 Nginx 配置 WebSocket 400 问题
查看>>
Glide和Govendor安装和使用
查看>>
Java全角、半角字符的关系以及转换
查看>>
Dubbo和Zookeeper
查看>>
前端项目课程3 jquery1.8.3到1.11.1有了哪些新改变
查看>>
UOJ#179. 线性规划(线性规划)
查看>>
整合spring cloud云架构 - SSO单点登录之OAuth2.0登录认证(1)
查看>>
windows的服务中的登录身份本地系统账户、本地服务账户和网络服务账户修改
查看>>
JAVA中循环删除list中元素的方法总结
查看>>
redis 安装
查看>>
SQL some any all
查看>>
电子书下载:Programming Windows Identity Foundation
查看>>